本篇目标:在 ROS2 中创建一个 Python 功能包,用 Qt 做图形界面,并通过订阅 ROS2 Topic 实时更新界面文字。

推荐技术组合:

  • ROS2 Python 客户端:rclpy
  • Qt Python 绑定:PySide6
  • 消息类型:std_msgs/msg/String
  • 运行方式:ros2 run status_display status_display

一、为什么不能直接照搬 C++ Qt 写法

原始 C++ 示例大概是:

#include <QApplication>
#include <QWidget>
#include <QLabel>

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    QLabel* label = new QLabel("");
    QString message = QString::fromStdString("Hello Qt!");
    label->setText(message);
    label->show();
    app.exec();
    return 0;
}

注意:QString::fromStdString 不能写成 QStubg::fromStdString

如果走 Python + ROS2,就不应该创建 rclcpp 包,而应该创建 ament_python 包,并使用 rclpy


二、创建 ROS2 Python 功能包

在工作空间的 src 目录下执行:

cd ~/chapt3_ws/topic_practice_ws/src
ros2 pkg create status_display --build-type ament_python --dependencies rclpy std_msgs --license Apache-2.0

创建后目录大致如下:

status_display/
├── package.xml
├── setup.py
├── setup.cfg
├── resource/
│   └── status_display
├── status_display/
│   └── __init__.py
└── test/

三、安装 Qt Python 依赖

推荐使用 PySide6

pip3 install PySide6

如果下载非常慢,说明正在从默认 PyPI 源拉取较大的 wheel 包。可以临时使用国内镜像:

pip3 install PySide6 -i https://mirrors.aliyun.com/pypi/simple/

如果在 Ubuntu / WSL2 中安装较慢,可以先确认 pip 可用:

python3 -m pip --version

如果缺少 pip:

sudo apt update
sudo apt install python3-pip -y

本文统一使用 Qt6,也就是 PySide6。后面的所有代码都按 PySide6 编写。


四、编写 Qt + ROS2 节点

新建文件:

touch ~/chapt3_ws/topic_practice_ws/src/status_display/status_display/status_display_node.py

写入以下代码:

import sys

import rclpy
from rclpy.node import Node
from std_msgs.msg import String

from PySide6.QtCore import QTimer
from PySide6.QtWidgets import QApplication, QLabel, QVBoxLayout, QWidget


class StatusDisplayWindow(QWidget):
    def __init__(self):
        super().__init__()

        self.setWindowTitle('ROS2 状态显示器')
        self.resize(420, 140)

        self.status_label = QLabel('等待 /status 消息...')
        self.status_label.setStyleSheet('font-size: 24px; padding: 20px;')

        layout = QVBoxLayout()
        layout.addWidget(self.status_label)
        self.setLayout(layout)

    def update_status(self, text: str):
        self.status_label.setText(text)


class StatusDisplayNode(Node):
    def __init__(self, window: StatusDisplayWindow):
        super().__init__('status_display')
        self.window = window

        self.subscription = self.create_subscription(
            String,
            'status',
            self.status_callback,
            10
        )

        self.get_logger().info('status_display 节点已启动,正在订阅 /status')

    def status_callback(self, msg: String):
        self.window.update_status(msg.data)
        self.get_logger().info(f'界面已更新:{msg.data}')


def main():
    rclpy.init()

    app = QApplication(sys.argv)
    window = StatusDisplayWindow()
    window.show()

    node = StatusDisplayNode(window)

    ros_timer = QTimer()
    ros_timer.timeout.connect(lambda: rclpy.spin_once(node, timeout_sec=0))
    ros_timer.start(10)

    exit_code = app.exec()

    node.destroy_node()
    rclpy.shutdown()
    sys.exit(exit_code)


if __name__ == '__main__':
    main()

五、为什么要用 QTimer + rclpy.spin_once

Qt 有自己的事件循环:

app.exec()

ROS2 也有自己的事件循环:

rclpy.spin(node)

这两个函数都会阻塞。如果直接这样写:

app.exec()
rclpy.spin(node)

后面的 ROS2 永远没机会运行。

如果这样写:

rclpy.spin(node)
app.exec()

后面的 Qt 窗口也永远没机会运行。

所以在 Qt 程序里,更适合使用:

rclpy.spin_once(node, timeout_sec=0)

再用 Qt 的 QTimer 每隔几毫秒调用一次它:

ros_timer = QTimer()
ros_timer.timeout.connect(lambda: rclpy.spin_once(node, timeout_sec=0))
ros_timer.start(10)

这样 Qt 界面保持响应,ROS2 消息也能被及时处理。


六、配置 setup.py

打开:

~/chapt3_ws/topic_practice_ws/src/status_display/setup.py

确保 entry_points 中有这个入口:

entry_points={
    'console_scripts': [
        'status_display = status_display.status_display_node:main',
    ],
},

七、配置 package.xml

打开:

~/chapt3_ws/topic_practice_ws/src/status_display/package.xml

确认包含:

<depend>rclpy</depend>
<depend>std_msgs</depend>

PySide6 通常通过 pip 安装,不一定写进 package.xml。如果你希望在文档上标明,也可以加:

<exec_depend>python3-pyside6</exec_depend>

八、编译功能包

回到工作空间根目录:

cd ~/chapt3_ws/topic_practice_ws
colcon build
source install/setup.bash

如果只想编译这一个包:

colcon build --packages-select status_display
source install/setup.bash

九、运行 Qt 状态显示器

启动界面节点:

ros2 run status_display status_display

此时会弹出一个 Qt 窗口,默认显示:

等待 /status 消息...

十、发布测试消息

另开一个终端,记得先 source:

cd ~/chapt3_ws/topic_practice_ws
source install/setup.bash

发布一条测试消息:

ros2 topic pub /status std_msgs/msg/String "{data: '机器人状态:正常运行'}" --once

如果一切正常,Qt 窗口里的文字会变成:

机器人状态:正常运行

持续发布:

ros2 topic pub /status std_msgs/msg/String "{data: '电量 80%,导航中'}" -r 1

-r 1 表示每秒发布 1 次。


十一、调试命令

查看话题是否存在:

ros2 topic list

查看 /status 消息内容:

ros2 topic echo /status

查看节点信息:

ros2 node info /status_display

查看消息类型:

ros2 interface show std_msgs/msg/String

十二、常见问题

12.1 运行时报 ModuleNotFoundError: No module named 'PySide6'

说明当前 Python 环境没有安装 PySide6:

pip3 install PySide6

如果你用了虚拟环境,要确保 ros2 run 用的是同一个 Python 环境。

12.2 窗口弹不出来

如果是在 WSL2 中运行 Qt,需要确认 Windows 侧支持 GUI 显示。Windows 11 的 WSLg 通常可以直接显示。

可以先测试:

python3 -c "from PySide6.QtWidgets import QApplication, QLabel; import sys; app=QApplication(sys.argv); w=QLabel('Qt OK'); w.show(); app.exec()"

如果这个测试都无法弹窗,问题不在 ROS2,而在 WSL GUI / Qt 环境。

12.3 ros2 run 找不到 status_display

通常是以下原因:

  1. 忘记执行 colcon build
  2. 忘记执行 source install/setup.bash
  3. setup.pyconsole_scripts 写错。
  4. Python 文件名或 main 函数名写错。

12.4 收不到 /status 消息

按顺序检查:

ros2 topic list
ros2 topic echo /status
ros2 node info /status_display

如果 ros2 topic echo /status 有数据,但界面不更新,重点检查:

  • 订阅的话题名是不是 status
  • 消息类型是不是 std_msgs/msg/String
  • QTimer 有没有调用 rclpy.spin_once

十三、最小运行流程汇总

cd ~/chapt3_ws/topic_practice_ws/src
ros2 pkg create status_display --build-type ament_python --dependencies rclpy std_msgs --license Apache-2.0

pip3 install PySide6

# 编写 status_display/status_display/status_display_node.py
# 修改 setup.py 的 console_scripts

cd ~/chapt3_ws/topic_practice_ws
colcon build --packages-select status_display
source install/setup.bash

ros2 run status_display status_display

另开终端:

cd ~/chapt3_ws/topic_practice_ws
source install/setup.bash
ros2 topic pub /status std_msgs/msg/String "{data: 'Hello Qt + ROS2 Python'}" --once

十四、显示前面章节的 CPU 使用信息

前面章节已经定义了自定义消息:

status_interfaces/msg/SystemStatus

发布者发布的话题是:

/system_status

消息字段包括:

builtin_interfaces/Time stamp
string host_name
float32 cpu_percent
float32 memory_percent
float32 memory_total
float32 memory_available
float64 net_sent
float64 net_recv

所以 Qt 显示端不应该再订阅 std_msgs/msg/String,而应该订阅:

from status_interfaces.msg import SystemStatus

14.1 安装 Qt 依赖

本章节继续使用 Qt6,也就是 PySide6。如果还没有安装,推荐先使用国内镜像安装:

pip3 install PySide6 -i https://mirrors.aliyun.com/pypi/simple/

如果想长期使用清华源,也可以先配置 pip:

pip3 config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
pip3 install PySide6

如果 Qt 界面里的中文显示成方框,说明系统缺少中文字体。WSL / Ubuntu 里安装中文字体:

sudo apt update
sudo apt install fonts-noto-cjk -y

安装后关闭当前 Qt 窗口,重新运行节点。


14.2 创建或修改 status_display

如果还没创建显示包:

cd ~/chapt3_ws/topic_practice_ws/src
ros2 pkg create status_display --build-type ament_python --dependencies rclpy status_interfaces --license Apache-2.0

如果已经创建过 status_display,就不用重复创建,只需要确认 package.xml 里有:

<depend>rclpy</depend>
<depend>status_interfaces</depend>

如果希望在依赖说明里标记 Qt6 运行依赖,也可以加:

<exec_depend>python3-pyside6</exec_depend>

不过 PySide6 通常通过 pip3 install PySide6 安装,实际教学中不写这一项也可以。


14.3 编写 CPU 状态 Qt 显示节点

新建文件:

touch ~/chapt3_ws/topic_practice_ws/src/status_display/status_display/cpu_status_display.py

写入:

import sys

import rclpy
from rclpy.node import Node
from status_interfaces.msg import SystemStatus

from PySide6.QtCore import QTimer
from PySide6.QtGui import QFont
from PySide6.QtWidgets import (
    QApplication,
    QLabel,
    QProgressBar,
    QVBoxLayout,
    QWidget,
)


class CpuStatusWindow(QWidget):
    def __init__(self):
        super().__init__()

        self.setWindowTitle('ROS2 系统状态监控')
        self.resize(920, 520)

        self.setStyleSheet("""
            QWidget {
                font-size: 24px;
            }
            QLabel {
                padding: 8px;
            }
            QProgressBar {
                min-height: 36px;
                text-align: center;
                font-size: 22px;
            }
        """)

        self.host_label = QLabel('主机名:等待数据...')
        self.cpu_label = QLabel('CPU:等待数据...')
        self.memory_label = QLabel('内存:等待数据...')
        self.net_label = QLabel('网络:等待数据...')

        self.cpu_bar = QProgressBar()
        self.cpu_bar.setRange(0, 1000)
        self.cpu_bar.setValue(0)

        self.memory_bar = QProgressBar()
        self.memory_bar.setRange(0, 1000)
        self.memory_bar.setValue(0)

        layout = QVBoxLayout()
        layout.addWidget(self.host_label)
        layout.addWidget(self.cpu_label)
        layout.addWidget(self.cpu_bar)
        layout.addWidget(self.memory_label)
        layout.addWidget(self.memory_bar)
        layout.addWidget(self.net_label)
        self.setLayout(layout)

    def update_status(self, msg: SystemStatus):
        self.host_label.setText(f'主机名:{msg.host_name}')

        cpu_percent = max(0.0, min(100.0, float(msg.cpu_percent)))
        memory_percent = max(0.0, min(100.0, float(msg.memory_percent)))

        cpu_value = int(cpu_percent * 10)
        memory_value = int(memory_percent * 10)

        self.cpu_label.setText(f'CPU 使用率:{cpu_percent:.1f}%')
        self.cpu_bar.setValue(cpu_value)
        self.cpu_bar.setFormat(f'{cpu_percent:.1f}%')

        memory_total_gb = msg.memory_total / 1024
        memory_available_gb = msg.memory_available / 1024
        self.memory_label.setText(
            f'内存使用率:{memory_percent:.1f}% '
            f'可用:{memory_available_gb:.2f} GB / 总量:{memory_total_gb:.2f} GB'
        )
        self.memory_bar.setValue(memory_value)
        self.memory_bar.setFormat(f'{memory_percent:.1f}%')

        self.net_label.setText(
            f'网络:发送 {msg.net_sent:.2f} MB,接收 {msg.net_recv:.2f} MB'
        )


class CpuStatusDisplayNode(Node):
    def __init__(self, window: CpuStatusWindow):
        super().__init__('cpu_status_display')
        self.window = window

        self.subscription = self.create_subscription(
            SystemStatus,
            'system_status',
            self.status_callback,
            10
        )

        self.get_logger().info('CPU 状态显示节点已启动,正在订阅 /system_status')

    def status_callback(self, msg: SystemStatus):
        self.window.update_status(msg)
        self.get_logger().info(
            f'收到系统状态:CPU {msg.cpu_percent:.1f}%, '
            f'内存 {msg.memory_percent:.1f}%'
        )


def main():
    rclpy.init()

    app = QApplication(sys.argv)
    app.setFont(QFont('Noto Sans CJK SC', 18))

    window = CpuStatusWindow()
    window.show()

    node = CpuStatusDisplayNode(window)

    ros_timer = QTimer()
    
    def spin_ros_once():
        if rclpy.ok():
            rclpy.spin_once(node, timeout_sec=0)

    ros_timer.timeout.connect(spin_ros_once)
    ros_timer.start(10)

    exit_code = app.exec()

    ros_timer.stop()
    node.destroy_node()
    if rclpy.ok():
        rclpy.shutdown()
    sys.exit(exit_code)


if __name__ == '__main__':
    main()

注意:Qt6 / PySide6 使用 app.exec()


14.4 配置 setup.py

打开:

~/chapt3_ws/topic_practice_ws/src/status_display/setup.py

把入口加入 console_scripts

entry_points={
    'console_scripts': [
        'cpu_status_display = status_display.cpu_status_display:main',
    ],
},

如果里面已经有其他入口,就写成:

entry_points={
    'console_scripts': [
        'status_display = status_display.status_display_node:main',
        'cpu_status_display = status_display.cpu_status_display:main',
    ],
},

14.5 编译

回到工作空间根目录:

cd ~/chapt3_ws/topic_practice_ws
colcon build
source install/setup.bash

如果之前只改了 status_display

colcon build --packages-select status_display
source install/setup.bash

如果提示找不到 status_interfaces,先确认接口包已经编译过:

colcon build --packages-select status_interfaces
source install/setup.bash

然后再编译显示包。


14.6 运行顺序

终端 1:启动 CPU 信息发布者。

cd ~/chapt3_ws/topic_practice_ws
source install/setup.bash
ros2 run status_publisher sys_status_pub

终端 2:启动 Qt 显示界面。

cd ~/chapt3_ws/topic_practice_ws
source install/setup.bash
ros2 run status_display cpu_status_display

如果正常,Qt 窗口会显示:

  • 主机名
  • CPU 使用率
  • 内存使用率
  • 可用内存 / 总内存
  • 网络发送 / 接收数据量

14.7 调试命令

查看系统状态话题是否存在:

ros2 topic list

查看发布者是否真的在发数据:

ros2 topic echo /system_status

查看自定义消息格式:

ros2 interface show status_interfaces/msg/SystemStatus

查看 Qt 显示节点:

ros2 node info /cpu_status_display

如果 ros2 topic echo /system_status 有数据,但 Qt 不更新,重点检查:

  1. status_display/package.xml 是否依赖了 status_interfaces
  2. Python 文件里是否导入了 from status_interfaces.msg import SystemStatus
  3. 订阅话题名是否写成了 system_status
  4. setup.py 的入口是否写成了 cpu_status_display = status_display.cpu_status_display:main
  5. 修改后是否重新 colcon buildsource install/setup.bash

更多推荐