Rviz教程(九):Plugins: New Tool Type

ROS1可视化利器---Webviz 对于ROS1而言,rqt和plotjuggler是我们最常用的工具,这两个工具:rqt中嵌入了很多有用的小工具,但是它需要播放离线包,没有办法对离线包进行实时的分析。而plotjuggler支持对离线bag包进行分析,但是不支持可视化的工具,这两个各有千秋。而近年来,有更多的工作开始落实在可视化上。 阅读详情

PlantFlagTool

Overview

This tutorial shows how to write a new tool for RViz.

In RViz, a tool is a class that determines how mouse events interact with the visualizer. In this example we describe PlantFlagTool which lets you place “flag” markers in the 3D scene.

The source code for this tutorial is in the rviz_plugin_tutorials package. You can check out the source directly or (if you use Ubuntu) you can just apt-get install the pre-compiled Debian package like so:

sudo apt-get install ros-hydro-visualization-tutorials

Here is an example of what the new PlantFlagTool can do:

_images/flags.png

The Plugin Code

The code for PlantFlagTool is in these files: src/plant_flag_tool.h, and src/plant_flag_tool.cpp.

plant_flag_tool.h

The full text of plant_flag_tool.h is here: src/plant_flag_tool.h

Here we declare our new subclass of rviz::Tool. Every tool which can be added to the tool bar is a subclass of rviz::Tool.

class PlantFlagTool: public rviz::Tool
{
Q_OBJECT
public:
  PlantFlagTool();
  ~PlantFlagTool();

  virtual void onInitialize();

  virtual void activate();
  virtual void deactivate();

  virtual int processMouseEvent( rviz::ViewportMouseEvent& event );

  virtual void load( const rviz::Config& config );
  virtual void save( rviz::Config config ) const;

private:
  void makeFlag( const Ogre::Vector3& position );

  std::vector<Ogre::SceneNode*> flag_nodes_;
  Ogre::SceneNode* moving_flag_node_;
  std::string flag_resource_;
  rviz::VectorProperty* current_flag_property_;
};

plant_flag_tool.cpp

The full text of plant_flag_tool.cpp is here: src/plant_flag_tool.cpp

Construction and destruction

The constructor must have no arguments, so we can’t give the constructor the parameters it needs to fully initialize.

Here we set the “shortcut_key_” member variable defined in the superclass to declare which key will activate the tool.

PlantFlagTool::PlantFlagTool()
  : moving_flag_node_( NULL )
  , current_flag_property_( NULL )
{
  shortcut_key_ = 'l';
}

The destructor destroys the Ogre scene nodes for the flags so they disappear from the 3D scene. The destructor for a Tool subclass is only called when the tool is removed from the toolbar with the “-” button.

PlantFlagTool::~PlantFlagTool()
{
  for( unsigned i = 0; i < flag_nodes_.size(); i++ )
  {
    scene_manager_->destroySceneNode( flag_nodes_[ i ]);
  }
}

onInitialize() is called by the superclass after scene_manager_ and context_ are set. It should be called only once per instantiation. This is where most one-time initialization work should be done. onInitialize() is called during initial instantiation of the tool object. At this point the tool has not been activated yet, so any scene objects created should be invisible or disconnected from the scene at this point.

In this case we load a mesh object with the shape and appearance of the flag, create an Ogre::SceneNode for the moving flag, and then set it invisible.

void PlantFlagTool::onInitialize()
{
  flag_resource_ = "package://rviz_plugin_tutorials/media/flag.dae";

  if( rviz::loadMeshFromResource( flag_resource_ ).isNull() )
  {
    ROS_ERROR( "PlantFlagTool: failed to load model resource '%s'.", flag_resource_.c_str() );
    return;
  }

  moving_flag_node_ = scene_manager_->getRootSceneNode()->createChildSceneNode();
  Ogre::Entity* entity = scene_manager_->createEntity( flag_resource_ );
  moving_flag_node_->attachObject( entity );
  moving_flag_node_->setVisible( false );
}

Activation and deactivation

activate() is called when the tool is started by the user, either by clicking on its button in the toolbar or by pressing its hotkey.

First we set the moving flag node to be visible, then we create an rviz::VectorProperty to show the user the position of the flag. Unlike rviz::Display, rviz::Tool is not a subclass of rviz::Property, so when we want to add a tool property we need to get the parent container with getPropertyContainer() and add it to that.

We wouldn’t have to set current_flag_property_ to be read-only, but if it were writable the flag should really change position when the user edits the property. This is a fine idea, and is possible, but is left as an exercise for the reader.

void PlantFlagTool::activate()
{
  if( moving_flag_node_ )
  {
    moving_flag_node_->setVisible( true );

    current_flag_property_ = new rviz::VectorProperty( "Flag " + QString::number( flag_nodes_.size() ));
    current_flag_property_->setReadOnly( true );
    getPropertyContainer()->addChild( current_flag_property_ );
  }
}

deactivate() is called when the tool is being turned off because another tool has been chosen.

We make the moving flag invisible, then delete the current flag property. Deleting a property also removes it from its parent property, so that doesn’t need to be done in a separate step. If we didn’t delete it here, it would stay in the list of flags when we switch to another tool.

void PlantFlagTool::deactivate()
{
  if( moving_flag_node_ )
  {
    moving_flag_node_->setVisible( false );
    delete current_flag_property_;
    current_flag_property_ = NULL;
  }
}

Handling mouse events

processMouseEvent() is sort of the main function of a Tool, because mouse interactions are the point of Tools.

We use the utility function rviz::getPointOnPlaneFromWindowXY() to see where on the ground plane the user’s mouse is pointing, then move the moving flag to that point and update the VectorProperty.

If this mouse event was a left button press, we want to save the current flag location. Therefore we make a new flag at the same place and drop the pointer to the VectorProperty. Dropping the pointer means when the tool is deactivated the VectorProperty won’t be deleted, which is what we want.

int PlantFlagTool::processMouseEvent( rviz::ViewportMouseEvent& event )
{
  if( !moving_flag_node_ )
  {
    return Render;
  }
  Ogre::Vector3 intersection;
  Ogre::Plane ground_plane( Ogre::Vector3::UNIT_Z, 0.0f );
  if( rviz::getPointOnPlaneFromWindowXY( event.viewport,
                                         ground_plane,
                                         event.x, event.y, intersection ))
  {
    moving_flag_node_->setVisible( true );
    moving_flag_node_->setPosition( intersection );
    current_flag_property_->setVector( intersection );

    if( event.leftDown() )
    {
      makeFlag( intersection );
      current_flag_property_ = NULL; // Drop the reference so that deactivate() won't remove it.
      return Render | Finished;
    }
  }
  else
  {
    moving_flag_node_->setVisible( false ); // If the mouse is not pointing at the ground plane, don't show the flag.
  }
  return Render;
}

This is a helper function to create a new flag in the Ogre scene and save its scene node in a list.

void PlantFlagTool::makeFlag( const Ogre::Vector3& position )
{
  Ogre::SceneNode* node = scene_manager_->getRootSceneNode()->createChildSceneNode();
  Ogre::Entity* entity = scene_manager_->createEntity( flag_resource_ );
  node->attachObject( entity );
  node->setVisible( true );
  node->setPosition( position );
  flag_nodes_.push_back( node );
}

Loading and saving the flags

Tools with a fixed set of Property objects representing adjustable parameters are typically just created in the tool’s constructor and added to the Property container (getPropertyContainer()). In that case, the Tool subclass does not need to override load() and save() because the default behavior is to read all the Properties in the container from the Config object.

Here however, we have a list of named flag positions of unknown length, so we need to implement save() and load() ourselves.

We first save the class ID to the config object so the rviz::ToolManager will know what to instantiate when the config file is read back in.

void PlantFlagTool::save( rviz::Config config ) const
{
  config.mapSetValue( "Class", getClassId() );

The top level of this tool’s Config is a map, but our flags should go in a list, since they may or may not have unique keys. Therefore we make a child of the map (flags_config) to store the list.

rviz::Config flags_config = config.mapMakeChild( "Flags" );

To read the positions and names of the flags, we loop over the the children of our Property container:

rviz::Property* container = getPropertyContainer();
int num_children = container->numChildren();
for( int i = 0; i < num_children; i++ )
{
  rviz::Property* position_prop = container->childAt( i );

For each Property, we create a new Config object representing a single flag and append it to the Config list.

rviz::Config flag_config = flags_config.listAppendNew();

Into the flag’s config we store its name:

flag_config.mapSetValue( "Name", position_prop->getName() );

... and its position.

    position_prop->save( flag_config );
  }
}

In a tool’s load() function, we don’t need to read its class because that has already been read and used to instantiate the object before this can have been called.

void PlantFlagTool::load( const rviz::Config& config )
{

Here we get the “Flags” sub-config from the tool config and loop over its entries:

rviz::Config flags_config = config.mapGetChild( "Flags" );
int num_flags = flags_config.listLength();
for( int i = 0; i < num_flags; i++ )
{
  rviz::Config flag_config = flags_config.listChildAt( i );

At this point each flag_config represents a single flag.

Here we provide a default name in case the name is not in the config file for some reason:

QString name = "Flag " + QString::number( i + 1 );

Then we use the convenience function mapGetString() to read the name from flag_config if it is there. (If no “Name” entry were present it would return false, but we don’t care about that because we have already set a default.)

flag_config.mapGetString( "Name", &name );

Given the name we can create an rviz::VectorProperty to display the position:

rviz::VectorProperty* prop = new rviz::VectorProperty( name );

Then we just tell the property to read its contents from the config, and we’ve read all the data.

prop->load( flag_config );

We finish each flag by marking it read-only (as discussed above), adding it to the property container, and finally making an actual visible flag object in the 3D scene at the correct position.

    prop->setReadOnly( true );
    getPropertyContainer()->addChild( prop );
    makeFlag( prop->getVector() );
  }
}

End of .cpp file

At the end of every plugin class implementation, we end the namespace and then tell pluginlib about the class. It is important to do this in global scope, outside our package’s namespace.

} // end namespace rviz_plugin_tutorials

#include <pluginlib/class_list_macros.h>
PLUGINLIB_EXPORT_CLASS(rviz_plugin_tutorials::PlantFlagTool,rviz::Tool )

Building the Plugin

To build the plugin, just do the normal “rosmake” thing:

rosmake rviz_plugin_tutorials

Exporting the Plugin

For the plugin to be found and understood by other ROS packages (in this case, rviz), it needs a “plugin_description.xml” file. This file can be named anything you like, as it is specified in the plugin package’s “manifest.xml” file like so:

<export>
    <rviz plugin="${prefix}/plugin_description.xml"/>
</export>

The contents of plugin_description.xml then look like this:

<library path="lib/librviz_plugin_tutorials">
  <class name="rviz_plugin_tutorials/Teleop"
         type="rviz_plugin_tutorials::TeleopPanel"
         base_class_type="rviz::Panel">
    <description>
      A panel widget allowing simple diff-drive style robot base control.
    </description>
  </class>
  <class name="rviz_plugin_tutorials/Imu"
         type="rviz_plugin_tutorials::ImuDisplay"
         base_class_type="rviz::Display">
    <description>
      Displays direction and scale of accelerations from sensor_msgs/Imu messages.
    </description>
    <message_type>sensor_msgs/Imu</message_type>
  </class>
  <class name="rviz_plugin_tutorials/PlantFlag"
         type="rviz_plugin_tutorials::PlantFlagTool"
         base_class_type="rviz::Tool">
    <description>
      Tool for planting flags on the ground plane in rviz.
    </description>
  </class>
</library>

The first line says that the compiled library lives in lib/librviz_plugin_tutorials (the ”.so” ending is appended by pluginlib according to the OS). This path is relative to the top directory of the package:

<library path="lib/librviz_plugin_tutorials">

The next section is a class entry describing the TeleopPanel:

<class name="rviz_plugin_tutorials/Teleop"
       type="rviz_plugin_tutorials::TeleopPanel"
       base_class_type="rviz::Panel">
  <description>
    A panel widget allowing simple diff-drive style robot base control.
  </description>
</class>

This specifies the name, type, base class, and description of the class. The name field must be a combination of the first two strings given to the PLUGINLIB_DECLARE_CLASS() macro in the source file. It must be the “package” name, a “/” slash, then the “display name” for the class. The “display name” is the name used for the class in the user interface.

The type entry must be the fully-qualified class name, including any namespace(s) it is inside.

The base_class_type is usually one of rviz::Panel, rviz::Display, rviz::Tool, or rviz::ViewController.

The description subsection is a simple text description of the class, which is shown in the class-chooser dialog and in the Displays panel help area. This section can contain HTML, including hyperlinks, but the markup must be escaped to avoid being interpreted as XML markup. For example a link tag might look like: &lt;a href="my-web-page.html"&gt;.

Display plugins can have multiple message_type tags, which are used by RViz when you add a Display by selecting it’s topic first.

Trying It Out

Once your RViz plugin is compiled and exported, simply run rviz normally:

rosrun rviz rviz

and rviz will use pluginlib to find all the plugins exported to it.

Add a PlantFlag tool by clicking on the “+” button in the toolbar and selecting “PlantFlag” from the list under your plugin package name (here it is “rviz_plugin_tutorials”).

Once “PlantFlag” is in your toolbar, click it or press “l” (the shortcut key) to start planting flags. Open the “Tool Properties” panel to see the positions of the flags you have planted.

Currently the only way to remove the flags is to delete the PlantFlag tool, which you do by pressing the “-” (minus sign) button in the toolbar and selecting “PlantFlag”.

Next Steps

PlantFlag as shown here is not terribly useful yet. Some extensions to make it more useful might be:

  • Add the ability to delete, re-position, and re-name existing flags.
  • Publish ROS messages with the names and locations of the flags.

To modify existing flags, you might:

  • Change processMouseEvent() to notice when the mouse is pointing near an existing flag.
  • When it is:
    • make the flag highlight.
    • If the right button is pressed, show a context menu with delete and rename options.
    • If the left button is pressed, begin dragging the existing flag around.

Conclusion

There are many possibilities for new types of interaction with RViz. We look forward to seeing what you make.

TurtleBot3 RViz可视化安装指南:ROS 1与ROS 2双路径实操 RViz(Robot Visualization)是ROS生态中核心的机器人三维可视化工具,其原理基于TF坐标变换、URDF模型解析与话题数据订阅,技术价值在于为SLAM、导航、感知等算法提供实时可观、可调、可验证的图形化调试环境。典型应用场景包括高校机器人实验、ROS认证备考及算法原型快速验证。本文聚焦TurtleBot3平台在RViz/RViz2中的开箱即用部署,深入解析URDF模型加载、TF树构建、激光雷达点云渲染等关键环节,并针对ROS 1 Noetic与ROS 2 Humble/Foxy双技术栈给 阅读详情

相关推荐

MoveIt基础:机械臂运动规划的工程化落地指南

机械臂运动规划是机器人应用的核心能力,其本质是在高维构型空间(C-space)中求解满足运动学、动力学与环境约束的安全轨迹。MoveIt作为ROS生态中唯一经工业验证的运动规划中间件,通过URDF/SRDF语义建模、OMPL算法插件化封装、ros_control执行桥接及rviz可视化调试四大机制,将复杂的数学问题转化为可配置、可复现、可维护的工程实践。它解决的不仅是‘能不能动’,更是‘动得是否安全、可靠、可解释’——尤其在抓取、装配等任务中,MoveIt提供的自碰撞检测、时间最优轨迹生成与失败恢复机制,显

xpp02的专栏 437

rviz_plugins_无人机路径规划3D插件_ROS_buildingd41_

可以在rosRVIZ 下使用,拓展2D功能。我借鉴别人的,侵删

MoveIt! Setup Assistant深度解剖:从配置工具到运动规划底层原理

MoveIt!是ROS生态中主流的机器人运动规划框架,其核心能力依赖于语义化配置体系——URDF描述物理结构、SRDF定义逻辑关系、OMPL提供规划算法、KDL或IKFast实现逆运动学求解。Setup Assistant并非图形化向导,而是将这些技术组件映射为可操作配置项的编译器,生成可版本管理的_moveit_config包。它强制用户理解规划组(Planning Group)与运动链的对应关系、自碰撞矩阵(Collision Matrix)对安全边界的建模逻辑、虚拟关节(Virtual Joint)对

张 作强 BLOG 464

【亲测免费】 RVizWeb - 将ROS可视化带入您的浏览器

RVizWeb - 将ROS可视化带入您的浏览器 项目介绍 RVizWeb 是一个创新的开源项目,将流行的ROS(机器人操作系统)可视化工具RViz的功能搬到了网页上。借助这个项目,您可以在任何支持现代浏览器的设备上,实现对ROS系统的实时监控和控制。无需安装复杂的桌面应用,只需通过简单的步骤启动服务,即可在浏览器中轻松访问。 项目技术分析 RVizWeb依赖于一系列强大的技术栈,包括: @js...

gitblog_00005的博客 2771

ROS单位与坐标系规范:米、弧度、右手系的工程强制约定

在机器人操作系统(ROS)中,'米'和'弧度'并非可选单位,而是分布式感知-决策-执行链路的物理一致性基石。其底层原理源于刚体运动建模对欧几里得空间的严格依赖:线性量用米保障浮点插值精度,角量用弧度确保三角函数数值稳定,配合右手笛卡尔坐标系维持旋转矩阵正交性。这一组合直接支撑TF树拓扑、传感器消息语义(如sensor_msgs/Imu)、URDF建模(xyz/rpy)及rviz可视化等核心能力。实际应用中,单位错配将导致TF树崩溃、IMU数据溢出、激光扫描畸变或机器人模型悬浮等典型故障。本文聚焦ROS原生约

ciqiaofu0192的博客 408

MoveIt!运动学配置实战指南:kinematics.yaml核心参数详解

运动学求解器是机器人运动规划的底层基础,其性能直接决定机械臂能否稳定、快速、鲁棒地完成位姿解算。理解IK(逆运动学)原理,需从刚体变换与雅可比矩阵出发,结合机器人自由度、关节限位及任务需求,在数值稳定性、求解速度与精度之间做出工程权衡。KDL与LMA等主流插件并非通用替代品,而是针对不同拓扑结构(如6DOF非冗余vs 7DOF冗余)设计的专用求解策略;而kinematics_solver_search_resolution、timeout、attempts等参数,本质上是对物理编码器精度、控制周期与实时性约

656

Rviz教程(八):Plugins: New Dockable Panel

TeleopPanel Overview This tutorial shows how to write a simple Panel(仪表板) plugin for RViz. A panel in RViz is a GUI widget(装饰物) which can be docked(使靠码头) in the main window or floating. It do

沫尘的博客 2840

MoveIt 2.8.0实战入门:ROS 2 Humble机械臂运动规划全链路解析

运动规划是机器人自主操作的核心能力,其本质是基于机器人模型与环境约束,在位姿空间或关节空间中搜索满足动力学与碰撞规避条件的可行轨迹。MoveIt作为ROS生态最成熟的运动规划中间件,采用分层架构(RobotModel→PlanningScene→MotionPlanningPipeline→ControllerManager),通过标准化接口实现算法解耦与硬件无关性。其技术价值在于支撑高鲁棒性、可扩展的工业级应用,如抓取、装配与打磨等任务。典型应用场景涵盖仿真验证、真实机械臂控制及实时伺服(moveit_s

weixin_34111790的博客 637

Rviz 的插件开发知识积累

个人在编写rviz插件的一些知识分享,希望对您有帮助,欢迎随时交流

Zeus1125的博客 2123

ROS核心函数库实战指南:从Robot Model到navigation的工程落地

ROS核心函数库是连接基础通信与真实机器人交付的关键技术层,涵盖Robot Model(URDF解析与TF坐标系构建)、Visualization(rviz调试闭环与数据时空一致性验证)、actionlib(长期任务状态管理)、pluginlib(算法热插拔与运行时替换)、nodelets(零拷贝高性能数据流)、navigation(自主导航全栈集成)及ROS-Industrial(工业确定性通信与安全接口)。这些模块共同构成ROS工程化能力底座,支撑移动抓取、AGV调度、协作装配等典型场景。理解其设计原理

diaogan4202的博客 481

Trac-IK运动学求解器原理与MoveIt!实战配置指南

机械臂运动学求解是机器人控制的核心基础,涉及正/逆运动学建模、雅可比矩阵分析、奇异点规避等关键概念。其技术原理直接影响路径规划成功率、实时响应性与物理执行鲁棒性。Trac-IK作为融合数值优化与多起点采样的混合型求解器,通过带约束的Levenberg-Marquardt算法、关节限位硬约束和多策略求解类型(Speed/Distance/Manipulation),显著提升6DOF/7DOF机械臂在工业抓取、视觉伺服、动态避障等真实场景中的求解成功率与稳定性。相比KDL易陷奇异点、IKFast缺乏灵活性的短板

weixin_30832405的博客 605

ROS中自定义RViz面板开发实战:从零构建可维护的控制与分析界面

RViz作为ROS核心可视化工具,其默认面板仅提供通用数据展示能力,难以满足特定机器人系统的参数调节、实时分析与业务指标嵌入等工程需求。自定义RViz面板本质是基于Qt框架扩展QWidget子类,通过继承rviz::Panel并对接ROS通信与Ogre渲染管线,实现UI逻辑与机器人状态的毫秒级同进程协同。该技术兼顾低延迟响应、原生渲染集成与ROS发行版兼容性,广泛应用于无人车轨迹调优、机械臂PID在线整定及手术机器人实时质量评估等场景。本文聚焦C++实现路径,详解插件注册、三层架构设计、配置持久化与跨线程安

weixin_30680385的博客 412

AI机械臂开发入门:从ROS2环境搭建到视觉抓取实战

机器人操作系统(ROS)作为机器人开发的分布式通信中间件和工具集,为构建复杂的机器人系统提供了硬件抽象、设备控制和进程通信等核心功能。其核心原理在于通过节点、话题、服务等机制实现模块化与解耦,从而高效整合感知、决策与控制模块。这一技术价值在于极大地降低了机器人软件的开发门槛,促进了算法复用和系统集成。在应用场景上,ROS广泛应用于服务机器人、自动驾驶、工业自动化及前沿的具身智能领域。本文聚焦于AI机械臂开发,以ROS2和MoveIt 2为核心工具链,详细拆解了如何将抽象的AI决策(如目标识别)通过运动规划转

weixin_34126557的博客 434

ubuntu—ROS入门

本文档是Ubuntu环境下ROS(Robot Operating System)的入门教程,基于B站「ROS入门」视频合集整理而成。教程内容系统全面,共分为十个主要章节: 基础准备:介绍Ubuntu常用命令、ROS安装步骤(noetic版本)、APT软件包管理及工作空间配置方法 开发工具:详细讲解VS Code安装配置、Terminator超级终端使用技巧 核心概念:阐述Package与Node的关系,包括软件包创建、节点管理等基础操作 进阶内容:涵盖launch文件编写、机器人运动控制、传感器数据处理(R

kangjianing1的博客 478

ROS知识:RViz Plugin 教程

rviz_plugin_tutorials 包为 rviz 构建了一个插件库,其中包含两个主要类:ImuDisplay 和 TeleopPanel。ImuDisplay:这是一个 rviz::Display 子类的示例,允许 rviz 显示来自 sensor_msgs::Imu 消息的数据。 ...

gongdiwudu的专栏 1万+

webviz:基于Web的可视化库

将您自己的bag文件拖放到以浏览您的机器人数据,或者使用连接到实时机器人或仿真。 Webviz是一个基于Web的应用程序,用于回放和可视化 。 该存储库还包含一些库,可以独立使用它们来构建基于Web的可视化工具。 webviz-core (,, ):一种检查工具。 regl-worldview (, , ):React库,用于使用渲染2D和3D场景。 @ cruise-automation / hooks ( npm ,github ):可重用的React钩子列表。 @ cruise-automation / button ( npm , github ):React按钮组件,支持破坏性动作,“脉冲”动画和布尔玛类的动画进度。 @ cruise-automation / tooltip ( npm , github ):React组件,使用popper.js向DOM元素添

Ubuntu 22.04 + ROS 2 Humble 项目通过 TRAE 改成 Ubuntu 202.04 + ROS 1 Noetic 项目的SKLL技能

Ubuntu 22.04 + ROS 2 Humble 项目通过 TRAE 改成 Ubuntu 202.04 + ROS 1 Noetic 项目的SKLL技能

苏凯的博客 87

2026年7月19日 从可插拔到共封装:技术路线研究报告

AI 算力的爆发正在重写数据中心光互连的技术路线。本报告对当前并存的四代光互连架构——

weixin_51184047的博客 589
上一篇: Rviz教程(八):Plugins: New Dockable Panel
下一篇: Rviz教程(十):Librviz: Incorporating RViz into a Custom GUI
沫尘雪痕
博客等级 码龄16年 166粉丝 0原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值