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真好啊,改了直接用