当前位置: 首页 > article >正文

ros动态调参界面的修改

使用的ros版本为 noetic

1. 修改界面布局

对应数字类型的调参界面,一行先后为 参数名称,最小值,水平滑动条,最大值,输入框。

有时候一个配置文件参数太多,参数名称与输入框隔得太远看起来不舒服,将输入框改到参数名称之后。

将下面修改后的 editor_number.ui 放到 /opt/ros/noetic/share/rqt_reconfigure/resource 下面即可使用。

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>_widget_top</class>
 <widget class="QWidget" name="_widget_top">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>393</width>
    <height>50</height>
   </rect>
  </property>
  <property name="minimumSize">
   <size>
    <width>300</width>
    <height>20</height>
   </size>
  </property>
  <property name="windowTitle">
   <string>Param</string>
  </property>
  <layout class="QVBoxLayout" name="verticalLayout">
   <property name="margin">
    <number>0</number>
   </property>
   <item>
    <layout class="QHBoxLayout" name="_layout_h" stretch="0,0,0,1,0">
     <item>
      <widget class="QLabel" name="_paramname_label">
       <property name="text">
        <string>param_name</string>
       </property>
      </widget>
     </item>
     <item>
      <widget class="QLineEdit" name="_paramval_lineEdit">
       <property name="sizePolicy">
        <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
         <horstretch>0</horstretch>
         <verstretch>0</verstretch>
        </sizepolicy>
       </property>
       <property name="minimumSize">
        <size>
         <width>75</width>
         <height>20</height>
        </size>
       </property>
      </widget>
     </item>
     <item>
      <widget class="QLabel" name="_min_val_label">
       <property name="minimumSize">
        <size>
         <width>30</width>
         <height>0</height>
        </size>
       </property>
       <property name="text">
        <string>min</string>
       </property>
       <property name="alignment">
        <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
       </property>
      </widget>
     </item>
     <item>
      <widget class="QSlider" name="_slider_horizontal">
       <property name="sizePolicy">
        <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
         <horstretch>0</horstretch>
         <verstretch>0</verstretch>
        </sizepolicy>
       </property>
       <property name="minimumSize">
        <size>
         <width>130</width>
         <height>0</height>
        </size>
       </property>
       <property name="pageStep">
        <number>1</number>
       </property>
       <property name="orientation">
        <enum>Qt::Horizontal</enum>
       </property>
      </widget>
     </item>
     <item>
      <widget class="QLabel" name="_max_val_label">
       <property name="minimumSize">
        <size>
         <width>30</width>
         <height>0</height>
        </size>
       </property>
       <property name="text">
        <string>max</string>
       </property>
      </widget>
     </item>
    </layout>
   </item>
  </layout>
 </widget>
 <resources/>
 <connections/>
</ui>

相比原版,item属性在文件中的先后顺序决定了各控件位置,layout的 stretch 属性后面 0,0,0,1,0表示第4个控件拉满填充布局中的剩余控件,这里就是滑动条了。

2. 对参数组做收起,搜索高亮功能

将下面文件param_groups.py放到  /opt/ros/noetic/lib/python3/dist-packages/rqt_reconfigure 下面即可,

# Copyright (c) 2012, Willow Garage, Inc.
# All rights reserved.
#
# Software License Agreement (BSD License 2.0)
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
#  * Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#  * Redistributions in binary form must reproduce the above
#    copyright notice, this list of conditions and the following
#    disclaimer in the documentation and/or other materials provided
#    with the distribution.
#  * Neither the name of Willow Garage, Inc. nor the names of its
#    contributors may be used to endorse or promote products derived
#    from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# Author: Isaac Saito, Ze'ev Klapow

import time
import threading

from python_qt_binding.QtCore import (QEvent, QMargins, QObject, QSize, Qt,
                                      Signal)
from python_qt_binding.QtGui import QFont, QIcon
from python_qt_binding.QtWidgets import (QFormLayout, QGroupBox,
                                         QHBoxLayout, QLabel, QPushButton,QCheckBox,
                                         QTabWidget, QVBoxLayout, QWidget,QComboBox, QStackedWidget,QLineEdit,QTreeWidget,QTreeWidgetItem)

from rqt_reconfigure import logging
# *Editor classes that are not explicitly used within this .py file still need
# to be imported. They are invoked implicitly during runtime.
from rqt_reconfigure.param_editors import (  # noqa: F401
    BooleanEditor, DoubleEditor, EDITOR_TYPES, EditorWidget, EnumEditor,
    IntegerEditor, StringEditor
)

_GROUP_TYPES = {
    '': 'BoxGroup',
    'collapse': 'CollapseGroup',
    'tab': 'TabGroup',
    'hide': 'HideGroup',
    'apply': 'ApplyGroup',
}


def find_cfg(config, name):
    """
    (Ze'ev) reaaaaallly cryptic function which returns the config object for
    specified group.
    """
    cfg = None
    for k, v in config.items():
        try:
            if k.lower() == name.lower():
                cfg = v
                return cfg
            else:
                try:
                    cfg = find_cfg(v, name)
                    if cfg:
                        return cfg
                except Exception as exc:
                    raise exc
        except AttributeError:
            pass
        except Exception as exc:
            raise exc
    return cfg


class GroupWidget(QWidget):
    """
    (Isaac's guess as of 12/13/2012)
    This class bonds multiple Editor instances that are associated with
    a single node as a group.
    """

    # public signal
    sig_node_disabled_selected = Signal(str)
    sig_node_state_change = Signal(bool)

    def __init__(self, updater, config, nodename):
        """
        :param config:
        :type config: Dictionary? defined in dynamic_reconfigure.client.Client
        :type nodename: str
        """
        super(GroupWidget, self).__init__()
        self.state = config['state']
        self.param_name = config['name']

        print('【GroupWidget】.init self.name {}, node name {}'.format(self.param_name,nodename))
        self._toplevel_treenode_name = nodename

        # TODO: .ui file needs to be back into usage in later phase.
#        ui_file = os.path.join(rp.get_path('rqt_reconfigure'),
#                               'resource', 'singlenode_parameditor.ui')
#        loadUi(ui_file, self)

        verticalLayout = QVBoxLayout(self)
        verticalLayout.setContentsMargins(QMargins(0, 0, 0, 0))

        _widget_nodeheader = QWidget()
        _h_layout_nodeheader = QHBoxLayout(_widget_nodeheader)
        _h_layout_nodeheader.setContentsMargins(QMargins(0, 0, 0, 0))

        self.nodename_qlabel = QLabel(self)
        font = QFont('Trebuchet MS, Bold')
        font.setUnderline(True)
        font.setBold(True)

        # Button to close a node.
        _icon_disable_node = QIcon.fromTheme('window-close')
        _bt_disable_node = QPushButton(_icon_disable_node, '', self)
        _bt_disable_node.setToolTip('Hide this node')
        _bt_disable_node_size = QSize(36, 24)
        _bt_disable_node.setFixedSize(_bt_disable_node_size)
        _bt_disable_node.pressed.connect(self._node_disable_bt_clicked)

        _h_layout_nodeheader.addWidget(self.nodename_qlabel)
        _h_layout_nodeheader.addWidget(_bt_disable_node)

        self.nodename_qlabel.setAlignment(Qt.AlignCenter)
        font.setPointSize(10)
        self.nodename_qlabel.setFont(font)
        grid_widget = QWidget(self)
        self.grid = QFormLayout(grid_widget)
        verticalLayout.addWidget(_widget_nodeheader)
        

        verticalLayout.addWidget(grid_widget, 1)
        # Again, these UI operation above needs to happen in .ui file.

        self.tab_bar = None  # Every group can have one tab bar
        self.tab_bar_shown = False

        self.updater = updater

        self.editor_widgets = []
        self._param_names = []

        self._create_node_widgets(config)

        logging.debug('Groups node name={}'.format(nodename))
        self.nodename_qlabel.setText(nodename)

        # Labels should not stretch
        # self.grid.setColumnStretch(1, 1)
        # self.setLayout(self.grid)
    def on_search_text_changed(self, text):
        """根据输入的文字来高亮某个 Widget"""
        # 重置所有 widget 的样式
        for widget in self.editor_widgets:
            widget.setStyleSheet("")
        
        # 如果输入的文本为空,恢复所有 widget 的默认样式
        if text.strip() == "":
            return
        
        # 检查每个 widget 是否包含搜索文字
        for widget in self.editor_widgets:
            print('Check highlight widget  {}'.format(widget.param_name.lower()))
            print('Check highlight text {}'.format(text.lower))
            if text.lower() in widget.param_name.lower():
                widget.setStyleSheet("background-color: yellow;")  # 高亮显示
            else:
                widget.setStyleSheet("")  # 恢复默认样式

    def collect_paramnames(self, config):
        pass

    # def on_item_expanded(self, item):
    #     """处理树节点展开事件"""
    #     widget = item.data(0, 1000)  # 获取存储的 QWidget
    #     if widget and not self.layout().contains(widget):
    #         widget.display(self)

    # def on_item_collapsed(self, item):
    #     """处理树节点折叠事件"""
    #     widget = item.data(0, 1000)  # 获取存储的 QWidget
    #     if widget and self.layout().contains(widget):
    #         self.layout().removeWidget(widget)
    #         widget.hide()

    def _create_node_widgets(self, config):
        """
        :type config: Dict?
        """

        i_debug = 0
        for param in config['parameters']:
            begin = time.time() * 1000
            editor_type = '(none)'

            if param['edit_method']:
                widget = EnumEditor(self.updater, param)
            elif param['type'] in EDITOR_TYPES:
                logging.debug('GroupWidget i_debug={} param type ={}'.format(
                              i_debug, param['type']))
                editor_type = EDITOR_TYPES[param['type']]
                widget = eval(editor_type)(self.updater, param)

            print('aaaaaaaaaa widget.name {}, type {}'.format(widget.param_name,param['type']))
            self.editor_widgets.append(widget)
            self._param_names.append(param['name'])
            
            logging.debug(
                'groups._create_node_widgets num editors={}'.format(i_debug))

            end = time.time() * 1000
            time_elap = end - begin
            logging.debug('ParamG editor={} loop=#{} Time={}msec'.format(
                editor_type, i_debug, time_elap))
            i_debug += 1

        print('{}, self.editor_widgets.size after params {}'.format(threading.current_thread(),len(self.editor_widgets)))
        for name, group in sorted(config['groups'].items()):
            if group['type'] == 'tab':
                print('11111111111')
                widget = TabGroup(
                    self, self.updater, group, self._toplevel_treenode_name)
            elif group['type'] in _GROUP_TYPES.keys():
                print('22222222222')
                widget = eval(_GROUP_TYPES[group['type']])(
                    self.updater, group, self._toplevel_treenode_name)
            else:
                print('33333333333 kind of group')
                widget = eval(_GROUP_TYPES[''])(
                    self.updater, group, self._toplevel_treenode_name)
            print('bbbbbbbbbbbb widget.name {}'.format(widget.param_name))
            self.editor_widgets.append(widget)
            logging.debug('groups._create_node_widgets name={}'.format(name))
        print('{}, self.editor_widgets.size after group {}'.format(threading.current_thread(),len(self.editor_widgets)))

        for i, ed in enumerate(self.editor_widgets):
            print('enumerate widget.param_name {}'.format(self.editor_widgets[i].param_name))
            ed.display(self.grid)
        print('{}, self.editor_widgets.size after enumerate {}'.format(threading.current_thread(),len(self.editor_widgets)))

        logging.debug('GroupWdgt._create_node_widgets'
                      ' len(editor_widgets)={}'.format(
                          len(self.editor_widgets)))

    
    def display(self, grid):
        grid.addRow(self)

    def update_group(self, config):
        if not config:
            return
        if 'state' in config:
            old_state = self.state
            self.state = config['state']
            if self.state != old_state:
                self.sig_node_state_change.emit(self.state)

        names = [name for name in config.keys()]

        for widget in self.editor_widgets:
            print('update_group widget.param_name {}'.format(widget.param_name))
            if isinstance(widget, EditorWidget):
                if widget.param_name in names:
                    widget.update_value(config[widget.param_name])
            elif isinstance(widget, GroupWidget):
                cfg = find_cfg(config, widget.param_name) or config
                widget.update_group(cfg)
    #     param_widgets = []
    #     for widget in self.editor_widgets:
    #         param_widgets.append(widget)
    #         # 将 QWidget 添加到 QStackedWidget 中
    #         self.stacked_widget.addWidget(widget)
    #         self.combo_box.addItem("show certain Widget")
        
    #     # 连接 QComboBox 的信号和槽
    #     self.combo_box.currentIndexChanged.connect(self.on_combobox_changed)
    #     # 布局
    #     layout = QVBoxLayout(self)
    #     layout.addWidget(self.combo_box)
    #     layout.addWidget(self.stacked_widget)


    #     for widget in self.editor_widgets:
    #         print('widget.param_name {}'.format(widget.param_name))
    #         if isinstance(widget, EditorWidget):
    #             if widget.param_name in names:
    #                 widget.update_value(config[widget.param_name])
    #         elif isinstance(widget, GroupWidget):
    #             cfg = find_cfg(config, widget.param_name) or config
    #             widget.update_group(cfg)

    # def on_combobox_changed(self, index):
    #     # 根据 QComboBox 的选择,切换 StackedWidget 显示的内容
    #     self.stacked_widget.setCurrentIndex(index)

        

    def close(self):
        for w in self.editor_widgets:
            w.close()

    def get_treenode_names(self):
        """
        :rtype: str[]
        """
        print('self._param_names {}'.format(self._param_names))
        return self._param_names

    def _node_disable_bt_clicked(self):
        logging.debug('param_gs _node_disable_bt_clicked')
        self.sig_node_disabled_selected.emit(self._toplevel_treenode_name)


class BoxGroup(GroupWidget):

    def __init__(self, updater, config, nodename):
        super(BoxGroup, self).__init__(updater, config, nodename)

        self.checkbox = QCheckBox('Show/Hide Label', self)
        self.checkbox.stateChanged.connect(self.on_checkbox_state_changed)

        self.grid.insertRow(0,self.checkbox)


        self.search_box = QLineEdit(self)
        self.search_box.setPlaceholderText("...")
        self.search_box.textChanged.connect(self.on_search_text_changed)
        self.grid.insertRow(0,QLabel("配置项:"), self.search_box)
        print('XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX')


        self.box = QGroupBox(self.param_name)
        self.box.setLayout(self.grid)

    def display(self, grid):
        grid.addRow(self.box)

    def on_checkbox_state_changed(self, state):
        for i in range(self.grid.count()):
            item = self.grid.itemAt(i)
            widget = item.widget()
            if widget is not None:
                if isinstance(widget, QCheckBox) == False or widget.text() != 'Show/Hide Label':
                    widget.setVisible(state == Qt.Checked)
 

class CollapseGroup(BoxGroup):

    def __init__(self, updater, config, nodename):
        super(CollapseGroup, self).__init__(updater, config, nodename)
        self.box.setCheckable(True)
        self.box.clicked.connect(self.click_cb)
        self.sig_node_state_change.connect(self.box.setChecked)
        print('YYYYYYYYYYYYYYYYYYYYYYYYYYYYY')

        for child in self.box.children():
            if child.isWidgetType():
                self.box.toggled.connect(child.setVisible)

        self.box.setChecked(self.state)

    def click_cb(self, on):
        self.updater.update({'groups': {self.param_name: on}})


class HideGroup(BoxGroup):

    def __init__(self, updater, config, nodename):
        super(HideGroup, self).__init__(updater, config, nodename)
        print('ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ')
        self.box.setVisible(self.state)
        self.sig_node_state_change.connect(self.box.setVisible)


class TabGroup(GroupWidget):

    def __init__(self, parent, updater, config, nodename):
        super(TabGroup, self).__init__(updater, config, nodename)
        self.parent = parent

        print('CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC')
        if not self.parent.tab_bar:
            self.parent.tab_bar = QTabWidget()

            # Don't process wheel events when not focused
            self.parent.tab_bar.tabBar().installEventFilter(self)

        self.wid = QWidget()
        self.wid.setLayout(self.grid)

        parent.tab_bar.addTab(self.wid, self.param_name)

    def eventFilter(self, obj, event):
        if event.type() == QEvent.Wheel and not obj.hasFocus():
            return True
        return super(GroupWidget, self).eventFilter(obj, event)

    def display(self, grid):
        if not self.parent.tab_bar_shown:
            grid.addRow(self.parent.tab_bar)
            self.parent.tab_bar_shown = True

    def close(self):
        super(TabGroup, self).close()
        self.parent.tab_bar = None
        self.parent.tab_bar_shown = False


class ApplyGroup(BoxGroup):
    class ApplyUpdater(QObject):

        pending_updates = Signal(bool)

        def __init__(self, updater, loopback):
            super(ApplyGroup.ApplyUpdater, self).__init__()
            self.updater = updater
            self.loopback = loopback
            self._configs_pending = {}
            print('FFFFFFFFFFFFFFFFFFFFFFFFFFFF')

        def update(self, config):
            for name, value in config.items():
                self._configs_pending[name] = value
            self.loopback(config)
            self.pending_updates.emit(bool(self._configs_pending))

        def apply_update(self):
            self.updater.update(self._configs_pending)
            self._configs_pending = {}
            self.pending_updates.emit(False)

    def __init__(self, updater, config, nodename):
        self.updater = ApplyGroup.ApplyUpdater(updater, self.update_group)
        super(ApplyGroup, self).__init__(self.updater, config, nodename)
        print('ffffffffffffffffffff')

        self.button = QPushButton('Apply %s' % self.param_name)
        self.button.clicked.connect(self.updater.apply_update)

        self.button.setEnabled(False)
        self.updater.pending_updates.connect(self._pending_cb)

        self.grid.addRow(self.button)

    def _pending_cb(self, pending_updates):
        if not pending_updates and self.button.hasFocus():
            # Explicitly clear focus to prevent focus from being
            # passed to the next in the chain automatically
            self.button.clearFocus()
        self.button.setEnabled(pending_updates)

python真好啊,改了直接用


http://www.kler.cn/a/518653.html

相关文章:

  • Flutter中PlatformView在鸿蒙中的使用
  • 一文了解二叉树的基本概念
  • Android - 通过Logcat Manager简单获取Android手机的Log
  • vim如何设置自动缩进
  • solidity基础 -- 事件
  • [Python学习日记-79] socket 开发中的粘包现象(解决模拟 SSH 远程执行命令代码中的粘包问题)
  • Linux内核中IPoIB驱动模块的初始化与实现
  • 什么是COLLATE排序规则?
  • WPF基础 | WPF 基础概念全解析:布局、控件与事件
  • 2025-01-22 Unity Editor 1 —— MenuItem 入门
  • 2025美赛数学建模MCM/ICM选题建议与分析,思路+模型+代码
  • 寒假1.23
  • springboot图书馆管理系统前后端分离版本
  • 程序员转型测试:解锁漏洞挖掘新旅程
  • Ubuntu终端CTRL+S被锁定后解锁快捷键
  • 【音视频处理】FFmpeg for Windows 安装教程
  • PHP explode函数基本用法
  • 数巅科技连续中标大模型项目 持续助力央国企数智化升级
  • 【机器学习】使用pytorch框架实现逻辑回归并保存模型,然后保存模型后再加载模型进行预测
  • 【机器学习】使用scikit-learn中的KNN包实现对鸢尾花数据集或者自定义数据集的的预测
  • 加速排查线上bug
  • YOLOv10-1.1部分代码阅读笔记-train.py
  • 【Elasticsearch 】 聚合分析:桶聚合
  • N-Tron恩畅交换机助力重庆江北机场安全网络升级
  • Vue3笔记——(三)hooks、路由
  • 7大主流语言二分搜索算法的不同实现对比