使用的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 Klapowimport time
import threadingfrom 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: F401BooleanEditor, 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 forspecified group."""cfg = Nonefor k, v in config.items():try:if k.lower() == name.lower():cfg = vreturn cfgelse:try:cfg = find_cfg(v, name)if cfg:return cfgexcept Exception as exc:raise excexcept AttributeError:passexcept Exception as exc:raise excreturn cfgclass GroupWidget(QWidget):"""(Isaac's guess as of 12/13/2012)This class bonds multiple Editor instances that are associated witha single node as a group."""# public signalsig_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 barself.tab_bar_shown = Falseself.updater = updaterself.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 = 0for param in config['parameters']:begin = time.time() * 1000editor_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() * 1000time_elap = end - beginlogging.debug('ParamG editor={} loop=#{} Time={}msec'.format(editor_type, i_debug, time_elap))i_debug += 1print('{}, 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:returnif 'state' in config:old_state = self.stateself.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 configwidget.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_namesdef _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 = parentprint('CCCCCCCCCCCCCCCCCCCCCCCCCCCCCC')if not self.parent.tab_bar:self.parent.tab_bar = QTabWidget()# Don't process wheel events when not focusedself.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 Truereturn 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 = Truedef close(self):super(TabGroup, self).close()self.parent.tab_bar = Noneself.parent.tab_bar_shown = Falseclass ApplyGroup(BoxGroup):class ApplyUpdater(QObject):pending_updates = Signal(bool)def __init__(self, updater, loopback):super(ApplyGroup.ApplyUpdater, self).__init__()self.updater = updaterself.loopback = loopbackself._configs_pending = {}print('FFFFFFFFFFFFFFFFFFFFFFFFFFFF')def update(self, config):for name, value in config.items():self._configs_pending[name] = valueself.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 automaticallyself.button.clearFocus()self.button.setEnabled(pending_updates)
python真好啊,改了直接用