简介:本资源是一个面向ROS初学者与机器人开发者的Qt+RVIZ跨平台可视化集成DEMO,聚焦Linux环境下Qt GUI与ROS可视化工具RVIZ的协同开发实践,解决自定义界面控制3D传感器数据展示、节点通信调试等典型工程问题。压缩包共14个文件,含4个核心CPP源码(如rvizwidget.cpp、qnode.cpp)、3个头文件(h)、2个UI界面设计文件(main_window.ui、rvizwidget.ui),辅以QRC资源描述、DOX文档及CMake构建配置,整体仅29KB,轻量易导入,结构清晰便于理解Qt Widgets与ROS节点生命周期管理、话题发布/订阅机制及RVIZ插件嵌入逻辑。目前已有575人学习下载,提供完整可编译工程框架,涵盖环境初始化、ROS节点封装、RVIZ Widget嵌入、UI交互事件绑定等关键实现,是掌握ROS人机交互界面开发的实用入门范例。
1. RVIZ + Qt + ROS Linux 环境下跑通一个可交互的可视化 Demo:为什么你 clone 下来却打不开、报错 fatal、界面空白或 VNC 里根本启动失败?
这不是一个“照着教程点几下就能跑”的玩具 Demo。当你在 Linux(尤其是 Ubuntu 20.04/22.04)上 git clone 一个名为rviz_demo_qtroslinux_rviz_DEMO_ROSRviz_ROS_的仓库,执行catkin_make后source devel/setup.bash,再敲rosrun rviz_demo demo_node—— 却只看到终端刷出qt.qpa.plugin: could not find the qt platform plugin "linuxfb"、fatal: cannot mix incompatible qt library (version ex50601)或者rviz: symbol lookup error: ... libQt5Core.so.5: undefined symbol: _ZNK7QString8toLatin1Ev,甚至 VNC 桌面里 rviz 窗口一闪而没……说明你正踩进 ROS+Qt 双版本混用的黑匣子深坑。这个标题不是炫技组合词,而是真实工程中高频复现的三重耦合故障现场:RVIZ 作为 ROS 官方可视化前端,依赖 Qt5;但 ROS Noetic 默认链接系统 Qt5,而你本地编译的 Qt 插件、或鱼香 ROS 一键安装脚本预装的 Qt、或自己从官网下载的 Qt Creator 二进制包,极大概率自带另一套 Qt5 运行时——它们彼此不兼容,且 Linux 下无 Windows 那种 DLL 路径优先级机制,一加载就崩。本文不讲抽象原理,只拆解:如何在标准 Ubuntu + ROS Noetic 环境下,从零构建一个带 Qt 自定义控件、能实时驱动 RVIZ 场景、且不翻车的最小可运行 Demo。适合正在调试 URDF 显示异常、想加按钮控制 TF 坐标系、或需要把 Fast-LIO 定位结果用 Qt 滑块手动微调后再发给 RVIZ 的一线开发者。
2. 为什么必须用 Qt5.12.8 + ROS Noetic 组合?不是 Qt6、不是 Qt5.15、更不是系统默认 Qt
2.1 ROS Noetic 的 Qt ABI 锁死机制:源码级绑定不可绕过
ROS Noetic(2020 年发布,LTS 至 2025)的rviz包在 CMakeLists.txt 中硬编码了find_package(Qt5 REQUIRED COMPONENTS Core Widgets OpenGL),且其rviz二进制在编译时通过-DQT_QMAKE_EXECUTABLE=/usr/lib/x86_64-linux-gnu/qt5/bin/qmake强制指向系统 Qt5。关键在于:Noetic 的 rviz.so 是用 Qt5.12.8 编译的(Ubuntu 20.04 默认)、Noetic 的 roscpp 和 rosconsole 也链接同一套 Qt 符号表。如果你强行用 Qt5.15 编译你的 Qt 控件(比如用 Qt Creator 4.15 自带的 Qt5.15.2),哪怕只是#include <QMainWindow>,生成的.so也会导出_ZN7QWidgetD1Ev@Qt_5.15这类符号,而 rviz 运行时只认_ZN7QWidgetD1Ev@Qt_5.12.8—— 动态链接器直接报undefined symbol。这不是版本号显示问题,是 ABI(Application Binary Interface)层面断裂。我曾用objdump -T your_plugin.so | grep QWidget对比过,Qt5.12.8 和 Qt5.15.2 的虚函数表偏移、RTTI 结构体布局全不同。
2.2 鱼香 ROS 一键安装的隐性陷阱:它帮你装了 Qt,但没帮你隔离环境
“鱼香 ROS 一键安装”(如curl -s https://fishros.com/install | bash)确实省去 ROS 安装步骤,但它默认会apt install qtbase5-dev qtchooser qt5-qmake qtbase5-dev-tools—— 这是系统 Qt5.12.8。但很多用户后续为开发 Qt 界面,又从 qt.io 下载离线安装包(如Qt 5.15.2 for Linux Desktop),并执行./qt-unified-linux-x64-4.4.2-online.run。该安装器默认把 Qt5.15.2 装到~/Qt/5.15.2/gcc_64/,并在~/.profile里追加:
export PATH="/home/user/Qt/5.15.2/gcc_64/bin:$PATH" export LD_LIBRARY_PATH="/home/user/Qt/5.15.2/gcc_64/lib:$LD_LIBRARY_PATH"这导致:qmake -v显示 5.15.2,which qmake指向新路径,但rosrun rviz rviz启动时,rviz 本身仍用/usr/lib/x86_64-linux-gnu/libQt5Core.so.5(5.12.8),而你的插件.so却链接了~/Qt/5.15.2/gcc_64/lib/libQt5Core.so.5(5.15.2)——dlopen()时动态链接器发现两个libQt5Core.so.5版本共存,直接触发fatal: cannot mix incompatible qt library。这是 Linux ELF 加载器的保护机制,非 bug,是 feature。
2.3 正确选型:锁定 Qt5.12.8 + Ubuntu 20.04/22.04 + ROS Noetic 三件套
| 组件 | 推荐版本 | 理由 | 验证命令 |
|---|---|---|---|
| OS | Ubuntu 20.04 LTS 或 22.04 LTS | 内核与 Mesa 驱动对 OpenGL 兼容性最佳,避免rviz: GLXBadContext | lsb_release -a |
| ROS | Noetic (1.15.15+) | 官方支持 Qt5.12.8,rosdep install自动解析正确依赖 | rosversion -d |
| Qt | 系统自带 Qt5.12.8(apt install qtbase5-dev) | 与 rviz ABI 完全一致,无需额外配置 | qmake -v& `dpkg -l |
| 构建工具 | catkin_tools(非catkin_make) | 支持--cmake-args -DQT_QMAKE_EXECUTABLE=/usr/lib/x86_64-linux-gnu/qt5/bin/qmake精确指定 qmake | catkin build --help |
提示:不要卸载系统 Qt5,也不要试图用
update-alternatives切换 qmake。ROS 工作空间必须全程使用/usr/lib/x86_64-linux-gnu/qt5/bin/qmake,这是唯一安全路径。
3. 从零构建可交互 RVIZ Demo:Qt 控件嵌入 RVIZ + ROS Topic 双向通信
3.1 创建 catkin 工作空间与基础包结构
我们不 clone 那个名字冗长的仓库(它大概率混用了 Qt5.15),而是手写一个最小可验证结构。目标:一个 Qt Widget,带一个QPushButton和一个QSlider,点击按钮发送/demo/cmd_vel,拖动滑块实时更新 RVIZ 中的 Arrow Marker。
mkdir -p ~/catkin_ws/src cd ~/catkin_ws/src catkin_create_pkg rviz_demo std_msgs geometry_msgs sensor_msgs roscpp rospy rviz qtbase5-dev cd ~/catkin_ws关键点:catkin_create_pkg显式声明qtbase5-dev为依赖,这会让rosdep install自动安装系统 Qt5.12.8 头文件和库,且CMakeLists.txt会自动添加find_package(Qt5 REQUIRED COMPONENTS Core Widgets OpenGL)。
3.2 编写 Qt 主窗口:继承 QMainWindow,但不直接 new QCoreApplication
RVIZ 是一个 Qt 应用,它自己管理QApplication实例。你的插件绝不能在main()里new QApplication(argc, argv),否则双 QApplication 冲突。正确做法:将 Qt 界面封装为QWidget子类,并在 ROS Node 中以QWidget::createWindowContainer()方式嵌入(适用于独立窗口)或直接作为 RVIZ Plugin(需继承rviz::Panel)。此处采用更轻量的独立窗口方案:
// src/rviz_demo/include/rviz_demo/demo_window.h #pragma once #include <QWidget> #include <QPushButton> #include <QSlider> #include <QVBoxLayout> #include <ros/ros.h> #include <geometry_msgs/Twist.h> namespace rviz_demo { class DemoWindow : public QWidget { Q_OBJECT public: explicit DemoWindow(QWidget *parent = nullptr); ~DemoWindow() override = default; private slots: void onButtonClicked(); void onSliderValueChanged(int value); private: QPushButton* button_; QSlider* slider_; ros::Publisher cmd_vel_pub_; ros::NodeHandle nh_; }; } // namespace rviz_demo// src/rviz_demo/src/demo_window.cpp #include "rviz_demo/demo_window.h" #include <QVBoxLayout> #include <QHBoxLayout> #include <QLabel> #include <geometry_msgs/Twist.h> namespace rviz_demo { DemoWindow::DemoWindow(QWidget *parent) : QWidget(parent), nh_("~") { setWindowTitle("RVIZ Demo Control Panel"); resize(300, 150); // 布局 QVBoxLayout* main_layout = new QVBoxLayout(this); QHBoxLayout* top_layout = new QHBoxLayout(); button_ = new QPushButton("Send Twist", this); top_layout->addWidget(button_); main_layout->addLayout(top_layout); QHBoxLayout* bottom_layout = new QHBoxLayout(); bottom_layout->addWidget(new QLabel("Linear X:", this)); slider_ = new QSlider(Qt::Horizontal, this); slider_->setRange(-100, 100); slider_->setValue(0); bottom_layout->addWidget(slider_); main_layout->addLayout(bottom_layout); // ROS Publisher cmd_vel_pub_ = nh_.advertise<geometry_msgs::Twist>("/demo/cmd_vel", 10); // 连接信号 connect(button_, &QPushButton::clicked, this, &DemoWindow::onButtonClicked); connect(slider_, &QSlider::valueChanged, this, &DemoWindow::onSliderValueChanged); } void DemoWindow::onButtonClicked() { geometry_msgs::Twist msg; msg.linear.x = 0.5; // 固定值测试 cmd_vel_pub_.publish(msg); ROS_INFO("Published cmd_vel: linear.x = 0.5"); } void DemoWindow::onSliderValueChanged(int value) { double linear_x = value / 100.0; // -1.0 ~ +1.0 geometry_msgs::Twist msg; msg.linear.x = linear_x; cmd_vel_pub_.publish(msg); ROS_INFO_STREAM("Slider updated: linear.x = " << linear_x); } } // namespace rviz_demo3.3 编写 ROS Node 主入口:启动 Qt 窗口并维持 ROS spin
// src/rviz_demo/src/demo_node.cpp #include <ros/ros.h> #include <QApplication> #include "rviz_demo/demo_window.h" int main(int argc, char** argv) { // 注意:这里 QApplication 必须在 ros::init 之后、且仅创建一次 ros::init(argc, argv, "rviz_demo_node"); if (!ros::ok()) return -1; // 关键:QApplication 必须传入 argc/argv,且不能重复创建 QApplication app(argc, argv); app.setAttribute(Qt::AA_EnableHighDpiScaling); // 高分屏适配 rviz_demo::DemoWindow window; window.show(); // 启动 ROS spinner 在后台线程,避免阻塞 Qt 事件循环 ros::AsyncSpinner spinner(2); // 2 个线程处理 callback spinner.start(); // 进入 Qt 事件循环(替代 ros::spin()) int result = app.exec(); spinner.stop(); return result; }3.4 配置 CMakeLists.txt:强制使用系统 Qt5.12.8
# CMakeLists.txt cmake_minimum_required(VERSION 3.0.2) project(rviz_demo) find_package(catkin REQUIRED COMPONENTS roscpp rospy std_msgs geometry_msgs sensor_msgs rviz qtbase5-dev # ← 显式声明,触发 rosdep 安装 ) # 查找 Qt5(必须指定路径,防止找到其他 Qt) find_package(Qt5 REQUIRED COMPONENTS Core Widgets OpenGL) set(CMAKE_AUTOMOC ON) set(CMAKE_CXX_STANDARD 14) # 添加头文件目录 include_directories( ${catkin_INCLUDE_DIRS} ${Qt5Core_INCLUDE_DIRS} ${Qt5Widgets_INCLUDE_DIRS} ${Qt5OpenGL_INCLUDE_DIRS} ) # 构建可执行文件 add_executable(demo_node src/demo_node.cpp src/demo_window.cpp) target_link_libraries(demo_node ${catkin_LIBRARIES} ${Qt5Core_LIBRARIES} ${Qt5Widgets_LIBRARIES} ${Qt5OpenGL_LIBRARIES} ) # 关键:强制 qmake 路径(即使系统只有一个 Qt5,也显式指定) set(QT_QMAKE_EXECUTABLE "/usr/lib/x86_64-linux-gnu/qt5/bin/qmake" CACHE STRING "")注意:
set(QT_QMAKE_EXECUTABLE ...)这行必须存在。它确保catkin build时 CMake 不会去$PATH里乱找 qmake,而是锁定系统 Qt5.12.8 的路径。漏掉这一行,catkin build可能静默使用你~/Qt/5.15.2/.../qmake,编译通过但运行时报错。
4. 编译与运行:避开 90% 的 fatal 报错和界面空白
4.1 严格按顺序执行的编译命令
cd ~/catkin_ws # 1. 确保 rosdep 已初始化(首次运行) sudo rosdep init rosdep update # 2. 安装所有依赖(含 qtbase5-dev) rosdep install --from-paths src --ignore-src -r -y # 3. 使用 catkin_tools 构建(推荐,比 catkin_make 更稳定) catkin build rviz_demo --cmake-args \ -DQT_QMAKE_EXECUTABLE=/usr/lib/x86_64-linux-gnu/qt5/bin/qmake \ -DCMAKE_BUILD_TYPE=RelWithDebInfo # 4. source 环境 source devel/setup.bash验证 Qt 路径是否生效:
catkin config --cmake-args | grep qmake # 应输出:-DQT_QMAKE_EXECUTABLE=/usr/lib/x86_64-linux-gnu/qt5/bin/qmake4.2 运行前必做的三项环境检查
检查 LD_LIBRARY_PATH 是否污染
echo $LD_LIBRARY_PATH # 如果包含 ~/Qt/5.15.2/gcc_64/lib 或 /opt/Qt/...,立即清空: unset LD_LIBRARY_PATH # 并从 ~/.bashrc 中注释掉相关 export 行确认 rviz 本身能独立启动
rosrun rviz rviz -h # 应正常打印帮助 # 若报错 qt.qpa.plugin,说明系统 Qt 缺失: sudo apt install qt5-default libqt5opengl5-dev验证 Qt 插件路径
RVIZ 启动时需加载libqxcb.so等平台插件。若报could not find the qt platform plugin "xcb",执行:# 查看 rviz 依赖的 Qt 库路径 ldd $(rospack find rviz)/lib/rviz/rviz | grep Qt5 # 输出应类似:libQt5Core.so.5 => /usr/lib/x86_64-linux-gnu/libQt5Core.so.5 (0x...) # 然后确认插件存在: ls /usr/lib/x86_64-linux-gnu/qt5/plugins/platforms/libqxcb.so # 若不存在,安装: sudo apt install qt5ct
4.3 启动 Demo 并联调 RVIZ
# 终端1:启动 RVIZ(加载一个空配置) rosrun rviz rviz -d $(rospack find rviz)/launch/empty.rviz # 终端2:运行我们的 Qt 控件 rosrun rviz_demo demo_node此时应看到:
- 独立弹出 Qt 窗口(非嵌入 RVIZ,便于调试)
- 点击按钮,RVIZ 中应出现
ArrowMarker(需提前在 RVIZ 中 Add → By Topic →/demo/cmd_vel→geometry_msgs/Twist,设置Arrow类型) - 拖动滑块,Arrow 长度实时变化
提示:RVIZ 的
TwistDisplay 默认不显示 Arrow,需右键Twistpanel →Edit→ 将Shape改为Arrow,Length设为1.0,Scale设为0.1。这是新手最常卡住的一步。
5. 避坑:5 条血泪经验总结,每一条都来自真实翻车现场
5.1 现象:fatal: cannot mix incompatible qt library (version ex50601)
原因:ex50601是 Qt5.12.8 的内部 ABI 标签(ex表示extended,50601是 Qt5.12.8 的 magic number)。此错误表明你的可执行文件(demo_node)链接了 Qt5.12.8 的libQt5Core.so.5,但某个动态库(如你写的.so插件、或第三方库)链接了 Qt5.15.x 的同名库。Linux 加载器拒绝混合。
解决:
ldd ./devel/lib/rviz_demo/demo_node | grep Qt5查看所有 Qt 库路径,确保全部指向/usr/lib/x86_64-linux-gnu/nm -D ./devel/lib/rviz_demo/demo_node | grep -i qt检查导出符号,确认无Qt_5.15字样- 彻底删除
~/Qt/目录,注释~/.bashrc中所有QT_*和LD_LIBRARY_PATH相关行
5.2 现象:Qt 窗口弹出即消失,终端无报错
原因:QApplication构造后未调用show(),或exec()前ros::init()失败导致app.exec()返回 0。
解决:
- 在
DemoWindow构造函数末尾加show();(代码中已有) - 在
main()中ros::init()后加ROS_ASSERT(ros::ok());,确保 ROS 初始化成功 - 若用 VNC,需设置
export QT_QPA_PLATFORM=xcb(而非linuxfb)
5.3 现象:RVIZ 中 Twist Marker 不动,但rostopic echo /demo/cmd_vel有数据
原因:RVIZ 的TwistDisplay 默认订阅/cmd_vel,而非/demo/cmd_vel。Topic 名不匹配。
解决:
- 在 RVIZ 中右键
Twistpanel →Edit→ 修改Topic字段为/demo/cmd_vel - 或在代码中改
nh_.advertise<...>("/cmd_vel", 10),保持与 RVIZ 默认一致
5.4 现象:滑块拖动时 RVIZ Marker 跳变剧烈,不平滑
原因:QSlider::valueChanged信号在拖动过程中高频触发,每毫秒发一次Twist,RVIZ 渲染跟不上。
解决:
- 在
onSliderValueChanged中加防抖:// 在头文件中声明 QTimer* timer_; // 构造函数中:timer_ = new QTimer(this); connect(timer_, &QTimer::timeout, this, &DemoWindow::publishTwist); void DemoWindow::onSliderValueChanged(int value) { current_value_ = value; // 成员变量缓存 timer_->start(50); // 50ms 延迟后才发 } void DemoWindow::publishTwist() { timer_->stop(); // ... 发布逻辑 }
5.5 现象:VNC 桌面无法启动 RVIZ,报GLXBadContext
原因:VNC 默认不启用 OpenGL 硬件加速,RVIZ 需要GLX扩展。
解决:
- 启动 VNC 时加参数:
vncserver :1 -localhost no -geometry 1920x1080 -depth 24 - 在 VNC 会话中运行:
export LIBGL_ALWAYS_INDIRECT=1 - 或改用
x11vnc替代tightvnc,它对 OpenGL 支持更好
6. 进阶技巧:让 Qt 控件真正嵌入 RVIZ 界面,而非独立窗口
6.1 为什么需要嵌入?独立窗口的三大硬伤
- 焦点冲突:Qt 窗口和 RVIZ 窗口互相抢夺键盘输入(如 WASD 移动视角时,Qt 滑块意外响应)
- 主题不一致:RVIZ 是深色主题,Qt 默认浅色,视觉割裂
- 部署麻烦:用户需同时启动两个进程,易遗漏
嵌入方案本质是:将你的 Qt Widget 注册为 RVIZ 的 Panel Plugin,它会出现在 RVIZ 右侧Panels区域,与Displays、Views并列,共享 RVIZ 的QApplication和主题。
6.2 创建 RVIZ Plugin 的最小骨架
RVIZ Plugin 必须继承rviz::Panel,且需注册为PLUGINLIB_EXPORT_CLASS。结构如下:
src/rviz_demo/ ├── include/rviz_demo/ │ ├── demo_panel.h # 继承 rviz::Panel │ └── demo_panel_widget.h # 纯 Qt Widget(不含 ROS) ├── src/ │ ├── demo_panel.cpp # 实现 Panel 接口 │ └── demo_panel_widget.cpp # 实现 UI 逻辑 └── plugin_description.xml # RVIZ 插件描述文件plugin_description.xml(必须):
<library path="librviz_demo"> <class name="rviz_demo/DemoPanel" type="rviz_demo::DemoPanel" base_class_type="rviz::Panel"> <description>A Qt-based control panel embedded in RVIZ.</description> </class> </library>demo_panel.h:
#pragma once #include <rviz/panel.h> #include <rviz_demo/demo_panel_widget.h> namespace rviz_demo { class DemoPanel : public rviz::Panel { Q_OBJECT public: explicit DemoPanel(QWidget* parent = nullptr); ~DemoPanel() override = default; // 必须实现,RVIZ 调用此函数创建 widget void load(const rviz::Config& config) override; void save(rviz::Config config) const override; private: DemoPanelWidget* widget_; // 真正的 Qt Widget }; } // namespace rviz_demodemo_panel.cpp:
#include "rviz_demo/demo_panel.h" #include <rviz/properties/property.h> #include <rviz/properties/property_tree_widget.h> namespace rviz_demo { DemoPanel::DemoPanel(QWidget* parent) : rviz::Panel(parent) { widget_ = new DemoPanelWidget(this); QVBoxLayout* layout = new QVBoxLayout(this); layout->addWidget(widget_); setLayout(layout); } void DemoPanel::load(const rviz::Config& config) { rviz::Panel::load(config); // 加载配置,如滑块初始值 int init_val; if (config.mapGetString("initial_slider_value", &init_val)) { widget_->setSliderValue(init_val); } } void DemoPanel::save(rviz::Config config) const { rviz::Panel::save(config); // 保存配置 config.mapSetValue("initial_slider_value", widget_->getSliderValue()); } } // namespace rviz_demo #include <pluginlib/class_list_macros.h> PLUGINLIB_EXPORT_CLASS(rviz_demo::DemoPanel, rviz::Panel)6.3 CMakeLists.txt 中的关键修改
# 添加 plugin 目标 add_library(rviz_demo SHARED src/demo_panel.cpp src/demo_panel_widget.cpp ) target_link_libraries(rviz_demo ${catkin_LIBRARIES} ${Qt5Core_LIBRARIES} ${Qt5Widgets_LIBRARIES} ${Qt5OpenGL_LIBRARIES} ) set_target_properties(rviz_demo PROPERTIES PREFIX "") # 安装 plugin 和 xml install(TARGETS rviz_demo ARCHIVE DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION} LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION} RUNTIME DESTINATION ${CATKIN_GLOBAL_BIN_DESTINATION} ) install(FILES plugin_description.xml DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION} )6.4 编译后启用 Plugin 的操作流程
- 编译:
catkin build rviz_demo - Source:
source devel/setup.bash - 启动 RVIZ:
rosrun rviz rviz Panels → Add New Panel → rviz_demo/DemoPanel- 面板即出现在右侧,与 RVIZ 原生 Panel 无异
我的习惯是:开发阶段用独立窗口(快速迭代 UI),交付前切到 Plugin 模式(专业感+免维护)。每次切换只需改
demo_node.cpp的main()函数,把QApplication和show()替换为rviz::VisualizationManager::getInstance()->getPanelFactoryManager()->addPanel(...)—— 但实际项目中,我直接维护两套main(),用#ifdef PLUGIN_MODE切换,省得反复改。希望帮到你。
本文还有配套的精品资源,点击获取