gpt4 book ai didi

python - 更新: How to reduce QWidget nesting in PyQt code?

转载 作者:行者123 更新时间:2023-12-01 07:26:52 25 4
gpt4 key购买 nike

更新的问题

我认为我最初的困境可能是我的 PyQt 应用程序的结构造成的。我创建 GUI 的方法是将较大的小部件分成较小的部分,每个部分都有自己的类,直到这些部分足够简单。因此,我最终得到了大量的嵌套,因为一个大的小部件包含较小的小部件的实例,而这些小部件又包含自己的更小的小部件。这使得在应用程序中导航数据变得困难。

PyQt 应用程序应该如何构建,以便在代码中易于理解,同时具有包含很少嵌套的结构?我还没有找到很多这样的例子,所以我'我有点卡住了。我最初问题中的代码示例显示了我当前使用的结构的一个很好的示例,该结构具有大量嵌套。

计划信息

GUI 用于创建一组用于运行测试的参数。每个设置中的选项应对应一个二进制数,并且将每组选项所指示的所有二进制数收集起来,形成单个二进制数序列并传递。对设置的更改不必在 session 之间延续,因为每个新 session 很可能对应于新的测试(从而对应于一组新的设置选择)。

应用程序的基本流程应该是,打开它后,所有可用设置(总共约 20 个)都设置为其默认值。用户可以浏览并更改他们想要的任何设置,完成后,他们可以按“生成”按钮来收集与设置相对应的所有二进制数并创建命令。实时预览随着设置更改而更新的各个位将非常有帮助,这就是更新必须立即进行的原因。

某些设置依赖于其他设置;例如,设置A有4个选项,如果选择选项3,则设置B应该可见,否则不可见。

原始问题

我绝对是 PyQt 的初学者,所以我不太知道我的问题措辞是否正确,但这里是。我有一个 GUI,其中我尝试采用一堆不同的设置,跟踪从每个设置中选择的数字,然后将数字传递给一个对象,该对象跟踪所有设置中的所有数字设置。问题是,可以这么说,我不知道将所有单独设置值添加到我的类树中的最佳方法。这是到目前为止我的 GUI 的结构:

  • 底部:单独的自定义 QWidget,每个 QWidget 负责一个设置。每个都有一个信号,每当它返回的值发生变化时就会触发。

  • :一个 QWidget,每个包含约 7-10 个单独的设置。这些将设置收集到相关组中。

  • 顶部:一个 QTabWidget,它将设置组的每个实例放入单独的选项卡中。该小部件还包含一个对象,理想情况下应将各个组中的所有设置收集到其中。

我的问题是如何从底层信号到顶层小部件获取值?我唯一的想法是将这些小设置小部件中的所有信号连接到中间层,并将中间层信号连接到顶层的某个东西。不过,这种链接看起来很疯狂。

我正在运行 PyQt5 和 Python 3.7。

这是一些精简的代码,希望能展示我想要做的事情。

class TabWindow(QTabWidget):
def __init__(self):
super().__init__()
self.tabs = [SettingsGroup1, SettingsGroup2, SettingsGroup3]
self.setting_storage = { # dictionary is where I'd like to store all settings values
# 'setting name': setting value
}
for tab in self.tabs:
self.addTab(tab, 'Example')


class SettingsGroup(QWidget):
def __init__(self):
super().__init__()
# not shown: layout created for widget
self.settings = []

def add_to_group(self, new_setting):
self.settings.append(new_setting)
# not shown: add setting to the layout


class SettingsGroup1(SettingsGroup):
def __init__(self):
super().__init__()
self.add_to_group([Setting1, Setting2, Setting3])

class SettingsGroup2(SettingsGroup):...

class SettingsGroup3(SettingsGroup):...


class Setting(QWidget):
val_signal = pyqtSignal([int], name='valChanged')

def __init__(self, name):
self.val = None
self.name = name


def set_val(self, new_val):
self.val = new_val
self.val_signal.emit(self.val) # <-- the signal I want to pass up


class Setting1(Setting):
def __init__(self, name):
super().__init__(name)
# not shown: create custom setting layout/interface

class Setting2(Setting):...

class Setting3(Setting):...

我使用了很多继承(SettingsGroup -> SettingsGroup1, 2, 3),因为每个子类都有自己独特的函数和内部依赖项。例如,对于每个设置子类,都有不同的用户界面。

感谢您提供的任何帮助!

最佳答案

编辑:问题已同时更新,我在此答案的底部添加了更具体的解决方案。

我觉得这个问题有点“基于意见”,但由于我也遇到过类似的情况,所以我想提出我的建议。在这些情况下,重要的是要了解,不存在一种做事的好方法,而是有很多做错的方法。

原始答案

一个想法可能是为每个“级别”创建一个通用信号接口(interface),它将获取该信号并通过添加自己的名称来跟踪设置“路径”,并将其发送回其父级;然后,最顶层的小部件将相应地评估更改。

在此示例中,每个选项卡“组”都有自己的 valueChanged 信号,其中包括组名称、设置名称和值;源信号从“源”(在本例中为旋转盒)发射,然后它跟随其父级,而父级又依次“添加”它们的名称。
请记住,您还可以为每个父级使用通用的 pyqtSignal(object) 并将其与 widget.valueChanged.connect(self.valueChanged) 连接,然后跟踪通过 self.sender() parent 向后行走来设置其组和设置。

最后请注意,如果您将这些值用于应用程序设置,请记住 Qt 已经提供了 QSettings API,它可以用作您需要在应用程序中设置(并在 session 之间记住)的每个配置的通用且操作系统透明的接口(interface)。我在示例中实现了它,但我建议您阅读其文档以更好地理解它是如何工作的。

import sys
from PyQt5 import QtCore, QtWidgets

class SettingWidget(QtWidgets.QWidget):
valueChanged = QtCore.pyqtSignal(int)
def __init__(self, name):
super().__init__()
self.settings = QtCore.QSettings()
self.val = 0
self.name = name
layout = QtWidgets.QVBoxLayout()
self.setLayout(layout)
layout.addWidget(QtWidgets.QLabel(self.name))
self.spinBox = QtWidgets.QSpinBox()
layout.addWidget(self.spinBox)
self.spinBox.valueChanged.connect(self.set_val)

def set_val(self, new_val):
if self.val != new_val:
self.val = new_val
self.valueChanged.emit(self.val)
# enter a setting group, ensuring that same name settings won't
# be mismatched; this allows a single sub level setting only
self.settings.beginGroup(self.parent().name)
self.settings.setValue(self.name, new_val)
# leave the setting group. THIS IS IMPORTANT!!!
self.settings.endGroup()

class SettingWidget1(SettingWidget):
def __init__(self):
super().__init__('Setting1')

class SettingWidget2(SettingWidget):
def __init__(self):
super().__init__('Setting2')

class SettingWidget3(SettingWidget):
def __init__(self):
super().__init__('Setting3')

class SettingsGroup(QtWidgets.QWidget):
# create two signal signatures, the first sends the full "path",
# while the last will just send the value
valueChanged = QtCore.pyqtSignal([str, str, int], [int])
def __init__(self, name):
super().__init__()
self.name = name
layout = QtWidgets.QHBoxLayout()
self.setLayout(layout)

def add_to_group(self, new_setting):
widget = new_setting()
# emit both signal signatures
widget.valueChanged.connect(
lambda value, name=widget.name: self.valueChanged.emit(
self.name, name, value))
widget.valueChanged.connect(self.valueChanged[int])
self.layout().addWidget(widget)

class SettingsGroup1(SettingsGroup):
def __init__(self):
super().__init__('Group1')
self.add_to_group(SettingWidget1)
self.add_to_group(SettingWidget2)

class SettingsGroup2(SettingsGroup):
def __init__(self):
super().__init__('Group2')
self.add_to_group(SettingWidget3)

class TabWidget(QtWidgets.QTabWidget):
def __init__(self):
QtWidgets.QTabWidget.__init__(self)
self.settings = QtCore.QSettings()
self.tabs = [SettingsGroup1, SettingsGroup2]
self.settingsDict = {}
for tab in self.tabs:
widget = tab()
self.addTab(widget, widget.__class__.__name__)
widget.valueChanged[str, str, int].connect(self.valueChangedFullPath)
widget.valueChanged[int].connect(self.valueChangedOnly)

def valueChangedFullPath(self, group, setting, value):
# update the settings dict; if the group key doesn't exist, create it
try:
self.settingsDict[group][setting] = value
except:
self.settingsDict[group] = {setting: value}
settingsData = [group, setting, value]
print('Full path result: {}'.format(settingsData))
# Apply setting from here, instead of using the SettingWidget
# settings.setValue() option; this allows a single sub level only
# self.applySetting(data)

def valueChangedOnly(self, value):
parent = sender = self.sender()
# sender() returns the last signal sender, so we need to track down its
# source; keep in mind that this is *not* a suggested approach, as
# tracking the source might result in recursion if the sender's sender
# is not one of its children; this system also has issues if you're
# using a Qt.DirectConnection from a thread different from the one that
# emitted it
while parent.sender() in sender.children():
parent = sender.sender()
widgetPath = []
while parent not in self.children():
widgetPath.insert(0, parent)
parent = parent.parent()
settingsData = [w.name for w in widgetPath] + [value]
print('Single value result: {}'.format(settingsData))
# similar to valueChangedFullPath(), but with this implementation more
# nested "levels" can be used instead
# self.applySetting(settingsData)

def applySetting(self, settingsData):
# walk up to the next to last of settingsData levels, assuming they are
# all parent group section names
for count, group in enumerate(settingsData[:-2], 1):
self.settings.beginGroup(group)
# set the setting name settingsData[-2] to its value settingsData[-1]
self.settings.setValue(*settingsData[-2:])
for g in range(count):
self.settings.endGroup()

if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
# set both Organization and Application name to make settings persistent
app.setOrganizationName('StackOverflow')
app.setApplicationName('Example')
w = TabWidget()
w.show()
sys.exit(app.exec_())

基于更新答案的替代解决方案

由于答案在更新中变得更加具体,因此我添加另一个建议。
据我们现在所知,您不需要那种级别的“嵌套”类,而是需要更专门设计的代码,可以根据您的目的重用。另外,由于您使用的是基于二进制的数据,只要您知道位操作的工作原理(我假设您知道)以及设置“小部件”,它就会使事情变得(双关语)更容易"不需要特定的 GUI 定制。

在此示例中,我仅创建了一个“设置”类和一个“组”类,并且它们的实例仅根据其名称和默认值创建。

import sys
from PyQt5 import QtCore, QtWidgets

defaultValues = '0010101', '1001010', '000111'
# set bit lengths for each setting; be careful in ensuring that each
# setting group has the full default value bit length!
groups = [
['Group 1', [1, 3, 2, 1]],
['Group 2', [1, 2, 2, 1, 1]],
['Group 1', [2, 1, 2, 1]],
]

class BinaryWidget(QtWidgets.QFrame):
changed = QtCore.pyqtSignal()
def __init__(self, name, index, defaults='0'):
QtWidgets.QFrame.__init__(self)
self.setFrameShape(self.StyledPanel|self.Sunken)
layout = QtWidgets.QGridLayout()
self.setLayout(layout)
self.index = index
self.defaults = defaults
self.buttons = []
# use the "defaults" length to create buttons
for i in range(len(defaults)):
value = int(defaults[i], 2) & 1
# I used QToolButtons as they're usually smaller than QPushButtons
btn = QtWidgets.QToolButton()
btn.setText(str(value))
layout.addWidget(btn, 1, i)
btn.setCheckable(True)
btn.setChecked(value)
btn.toggled.connect(self.changed)
# show the binary value on change, just for conveniency
btn.toggled.connect(lambda v, btn=btn: btn.setText(str(int(v))))
self.buttons.append(btn)
layout.addWidget(QtWidgets.QLabel(name), 0, 0, 1, layout.columnCount())

def value(self):
# return the correct value of all widget's buttons; they're reversed
# because of how bit shifting works
v = 0
for i, btn in enumerate(reversed(self.buttons)):
v += btn.isChecked() << i
# bit shift again, according to the actual "setting" bit index
return v << self.index

def resetValues(self):
oldValue = self.value()
self.blockSignals(True)
for i, value in enumerate(self.defaults):
self.buttons[i].setChecked(int(self.defaults[i], 2) & 1)
self.blockSignals(False)
newValue = self.value()
# emit the changed signal only once, and only if values actually changed
if oldValue != newValue:
self.changed.emit()

class Group(QtWidgets.QWidget):
changed = QtCore.pyqtSignal()
def __init__(self, name, defaults=None, lenghts=None):
QtWidgets.QWidget.__init__(self)
layout = QtWidgets.QHBoxLayout()
self.setLayout(layout)
self.name = name
self.bitLength = 0
self.widgets = []
if defaults is not None:
self.addOptions(defaults, lenghts)

def value(self):
v = 0
for widget in self.widgets:
v += widget.value()
return v

def addOption(self, name, index, default='0'):
widget = BinaryWidget(name, index, default)
self.layout().addWidget(widget)
self.widgets.append(widget)
widget.changed.connect(self.changed)
self.bitLength += len(default)

def addOptions(self, defaults, lenghts = None):
if lenghts is None:
lenghts = [1] * len(defaults)
# reverse bit order for per-setting indexing
defaultsIndex = 0
bitIndex = len(defaults)
for i, l in enumerate(lenghts):
self.addOption(
'Setting {}'.format(i + 1),
bitIndex - l,
defaults[defaultsIndex:defaultsIndex + l])
bitIndex -= l
defaultsIndex += l

def resetValues(self):
for widget in self.widgets:
widget.resetValues()

class Tester(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
layout = QtWidgets.QGridLayout()
self.setLayout(layout)

self.tabWidget = QtWidgets.QTabWidget()
layout.addWidget(self.tabWidget)

resultLayout = QtWidgets.QHBoxLayout()
layout.addLayout(resultLayout, layout.rowCount(), 0, 1, layout.columnCount())

self.tabs = []
self.labels = []

for (group, lenghts), defaults in zip(groups, defaultValues):
tab = Group(group, defaults, lenghts)
self.tabWidget.addTab(tab, group)
tab.changed.connect(self.updateResults)
self.tabs.append(tab)
tabLabel = QtWidgets.QLabel()
self.labels.append(tabLabel)
resultLayout.addWidget(tabLabel)

self.resetButton = QtWidgets.QPushButton('Reset values')
layout.addWidget(self.resetButton)
self.resetButton.clicked.connect(lambda: [tab.resetValues() for tab in self.tabs])

self.updateResults()

def values(self):
return [tab.value() for tab in self.tabs]

def updateResults(self):
for value, tab, label in zip(self.values(), self.tabs, self.labels):
label.setText('''
{0}: <span style="font-family:monospace;">{1} <b>{1:0{2}b}</b></span>
'''.format(tab.name, value, tab.bitLength))

if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = Tester()
w.show()
sys.exit(app.exec_())

关于python - 更新: How to reduce QWidget nesting in PyQt code?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57401418/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com