Skip to content

RViz 自定义 Panel

本教程面向希望在 RViz 中以二维形式显示数据、或与数据进行交互的用户。

在本教程中,你将学习如何在 RViz 中完成以下三件事:

  • 创建一个新的 Qt Panel。
  • 创建一个话题订阅者(Subscriber),用于监控指定话题上的消息,并将其显示在 Panel 中。
  • 创建一个话题发布者(Publisher),使得 Panel 中的按钮按下后能向 ROS 话题发布消息。

本教程的所有代码可以在 此仓库 中找到。

以下是 demo_panel.hpp 的内容:

#ifndef RVIZ_PANEL_TUTORIAL__DEMO_PANEL_HPP_
#define RVIZ_PANEL_TUTORIAL__DEMO_PANEL_HPP_
#include <rviz_common/panel.hpp>
namespace rviz_panel_tutorial
{
class DemoPanel
: public rviz_common::Panel
{
Q_OBJECT
public:
explicit DemoPanel(QWidget * parent = 0);
~DemoPanel() override;
};
} // namespace rviz_panel_tutorial
#endif // RVIZ_PANEL_TUTORIAL__DEMO_PANEL_HPP_
  • 我们继承自 rviz_common::Panel 类。
  • 由于一些不在本教程讨论范围内的原因,你需要在类中添加 Q_OBJECT 宏,Qt 的 GUI 机制才能正常工作。
  • 暂时只声明构造函数和析构函数,它们的实现放在 cpp 文件中。

demo_panel.cpp:

#include <rviz_panel_tutorial/demo_panel.hpp>
namespace rviz_panel_tutorial
{
DemoPanel::DemoPanel(QWidget* parent) : Panel(parent)
{
}
DemoPanel::~DemoPanel() = default;
} // namespace rviz_panel_tutorial
#include <pluginlib/class_list_macros.hpp>
PLUGINLIB_EXPORT_CLASS(rviz_panel_tutorial::DemoPanel, rviz_common::Panel)
  • 重写构造函数和析构函数并非必需,但后续我们会在其中添加更多逻辑。
  • 为了让 RViz 能发现我们的插件,需要在代码中添加这个 PLUGINLIB 调用(以及下面的其他配置)。

我们需要在 package.xml 中添加以下依赖:

<depend>pluginlib</depend>
<depend>rviz_common</depend>
<library path="demo_panel">
<class type="rviz_panel_tutorial::DemoPanel" base_class_type="rviz_common::Panel">
<description></description>
</class>
</library>
  • 这是标准的 pluginlib 代码。

    • library 的 path 是我们将在 CMake 中指定的库名称。
    • class 应与上面的 PLUGINLIB 调用保持一致。
  • 描述内容稍后补充。

在标准模板基础上,添加以下内容:

find_package(ament_cmake_ros REQUIRED)
find_package(pluginlib REQUIRED)
find_package(rviz_common REQUIRED)
set(CMAKE_AUTOMOC ON)
qt5_wrap_cpp(MOC_FILES
include/rviz_panel_tutorial/demo_panel.hpp
)
add_library(demo_panel src/demo_panel.cpp ${MOC_FILES})
target_include_directories(demo_panel PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
target_link_libraries(demo_panel PUBLIC
pluginlib::pluginlib
rviz_common::rviz_common
)
install(TARGETS demo_panel
EXPORT export_rviz_panel_tutorial
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin
)
install(DIRECTORY include/
DESTINATION include
)
install(FILES rviz_common_plugins.xml
DESTINATION share/${PROJECT_NAME}
)
ament_export_include_directories(include)
ament_export_targets(export_rviz_panel_tutorial)
pluginlib_export_plugin_description_file(rviz_common rviz_common_plugins.xml)
  • 为了生成正确的 Qt 文件,我们需要:

    • 开启 CMAKE_AUTOMOC。
    • 通过调用 qt5_wrap_cpp 包装每个包含 Q_OBJECT 的头文件。
    • 将 MOC_FILES 与其他 cpp 文件一起包含到库中。
  • 其余大量代码用于保证插件机制正常运作。 其中,pluginlib_export_plugin_description_file 调用尤为关键——RViz 正是通过它来发现你的新插件的。

编译代码,source 你的工作空间,然后运行 rviz2。

顶部菜单栏中应该有一个「Panels」菜单。 从中选择「Add New Panel」。

{/截图:Add New Panel 对话框/}

此时会弹出一个对话框,列出 ROS 环境中所有可用的 Panel,按所属 ROS 包分组显示。 双击名称或选中后点击 OK,即可创建 Panel 的新实例。

这会在 RViz 窗口中添加一个新 Panel,不过目前只有一个带 Panel 名称的标题栏,还没有实际内容。

{/截图:RViz 窗口显示新的简单 Panel/}

接下来为 Panel 添加一些基本的 ROS/Qt 交互功能。 整体思路是:从 RViz 内部访问 ROS 节点,用该节点同时实现话题的订阅与发布。 我们用订阅者来监听 /input 话题,并将收到的 String 消息显示在组件中; 用发布者将按钮点击事件映射为向 /output 话题发布的消息。

更新 demo_panel.hpp,包含以下头文件和类体。

#include <rviz_common/panel.hpp>
#include <rviz_common/ros_integration/ros_node_abstraction_iface.hpp>
#include <std_msgs/msg/string.hpp>
#include <QLabel>
#include <QPushButton>
namespace rviz_panel_tutorial
{
class DemoPanel : public rviz_common::Panel
{
Q_OBJECT
public:
explicit DemoPanel(QWidget * parent = 0);
~DemoPanel() override;
void onInitialize() override;
protected:
std::shared_ptr<rviz_common::ros_integration::RosNodeAbstractionIface> node_ptr_;
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
void topicCallback(const std_msgs::msg::String & msg);
QLabel* label_;
QPushButton* button_;
private Q_SLOTS:
void buttonActivated();
};
} // namespace rviz_panel_tutorial
  • ROS 相关部分:我们声明了一个抽象节点指针,用它来与 ROS 生态系统交互。 订阅者用于从 ROS 获取信息并在 RViz 中使用;发布者则用于从 RViz 向 ROS 发布消息或事件。 此外还有用于初始化 ROS 组件的 onInitialize 方法,以及订阅者回调 topicCallback。
  • Qt 相关部分:声明了一个标签(Label)和一个按钮(Button),以及按钮的回调 buttonActivated。

更新 demo_panel.cpp 为以下内容:

#include <rviz_panel_tutorial/demo_panel.hpp>
#include <QVBoxLayout>
#include <rviz_common/display_context.hpp>
namespace rviz_panel_tutorial
{
DemoPanel::DemoPanel(QWidget* parent) : Panel(parent)
{
// 创建标签和按钮,垂直显示(VBox 中的 V 表示垂直)
const auto layout = new QVBoxLayout(this);
// 为按钮创建标签和按钮
label_ = new QLabel("[no data]");
button_ = new QPushButton("GO!");
// 将这些元素添加到 GUI 布局中
layout->addWidget(label_);
layout->addWidget(button_);
// 将按钮释放的事件连接到我们的回调,
// 这样按下按钮就会调用 buttonActivated 回调。
QObject::connect(button_, &QPushButton::released, this, &DemoPanel::buttonActivated);
}
DemoPanel::~DemoPanel() = default;
void DemoPanel::onInitialize()
{
// 获取抽象的 ROS Node 并
// 在此过程中锁定它以供独占使用,直到方法结束。
node_ptr_ = getDisplayContext()->getRosNodeAbstraction().lock();
// 获取熟悉的 rclcpp::Node 指针用于创建订阅者/发布者
// (与普通的 rclcpp 代码一样)
rclcpp::Node::SharedPtr node = node_ptr_->get_raw_node();
// 为输出创建一个 String 发布者
publisher_ = node->create_publisher<std_msgs::msg::String>("/output", 10);
// 创建一个 String 订阅者并将其绑定到此类内部的 topicCallback。
subscription_ = node->create_subscription<std_msgs::msg::String>("/input", 10, std::bind(&DemoPanel::topicCallback, this, std::placeholders::_1));
}
// 当订阅者收到消息时,此回调被触发,
// 然后我们将其数据复制到组件的标签中
void DemoPanel::topicCallback(const std_msgs::msg::String & msg)
{
label_->setText(QString(msg.data.c_str()));
}
// 当组件的按钮被按下时,此回调被触发,
// 然后我们在我们的 Topic 上发布一条新消息。
void DemoPanel::buttonActivated()
{
auto message = std_msgs::msg::String();
message.data = "Button clicked!";
publisher_->publish(message);
}
} // namespace rviz_panel_tutorial
#include <pluginlib/class_list_macros.hpp>
PLUGINLIB_EXPORT_CLASS(rviz_panel_tutorial::DemoPanel, rviz_common::Panel)

再次编译并启动 RViz2,加载你的 Panel。 现在你应该能在 Panel 中看到标签和按钮了。

{/截图:RViz Panel 的默认状态/}

要更新标签内容,只需在 /input 话题上发布一条消息:

Terminal window
ros2 topic pub /input std_msgs/msg/String "{data: 'Please be kind.'}"

由于组件订阅了该话题,消息到达时就会触发回调,从而更新标签文本。

{/截图:RViz Panel 显示自定义字符串消息/}

按下按钮会发布一条消息,你可以用以下命令通过 echo /output 话题来查看:

Terminal window
ros2 topic echo /output

接下来做一些清理工作。 这一步并非必需,但能让界面更美观、更易用。

首先,更新 rviz_common_plugins.xml 中插件的描述。

此外,我们在 icons/classes/DemoPanel.png 处为插件添加一个图标。 文件夹路径是硬编码的,文件名应与插件声明中的名称一致(若未指定则为类名)。

然后在 CMake 中安装该图片文件:

install(FILES icons/classes/DemoPanel.png
DESTINATION share/${PROJECT_NAME}/icons/classes
)

现在添加 Panel 时,应该能看到图标和描述了。

{/截图:Add New Panel 对话框,显示自定义图标和描述/}

Panel 标题栏也会显示更新后的图标。

{/截图:RViz Panel 显示自定义图标/}