更新:如何减少PyQt代码中的QWidget嵌套?

2024-03-28 07:55:18 发布

您现在位置:Python中文网/ 问答频道 /正文

更新的问题

我想我最初的窘境可能是我的PyQt应用程序的结构造成的。我创建GUI的方法是将较大的小部件分成较小的部分,每个小部件都有自己的类,直到这些部分足够简单为止。正因为如此,我最终得到了大量的嵌套,因为一个大的小部件包含了一些小部件的实例,而那些小部件则包含了它们自己的小部件。这使得在应用程序中浏览数据变得困难。你知道吗

PyQt应用程序应该如何构造,使其在代码中易于理解,同时又具有包含很少嵌套的结构?我还没有找到很多这样的例子,所以我有点困了。我最初问题中的代码示例展示了我当前使用的结构的一个非常好的示例,它有大量嵌套。你知道吗

节目信息

GUI用于创建一组用于运行测试的参数。每个设置中的选项都应该对应于一个二进制数,并且每个选项集所指示的所有二进制数都被收集起来,形成一个二进制数序列,然后传递下去。对设置的更改不必在会话之间进行,因为每个新会话都很可能对应于一个新的测试(以及一组新的设置选项)。你知道吗

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

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

原始问题

我绝对是PyQt的初学者,所以我不太清楚我的问题措辞是否正确,但这里是。我有一个GUI,在其中我尝试进行一系列不同的设置,跟踪从每个设置中选择的数字,然后将数字传递给一个对象,该对象跟踪所有设置中的所有数字。问题是,可以说,我不知道如何将所有的设置值都放到我的类树中。以下是迄今为止我的GUI的结构:

  • 底部:单个自定义Qwidget,每个都负责一个设置。每一个都有一个信号,只要返回的值发生变化就触发。

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

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

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

我正在运行PyQt5和python3.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),因为每个子类都有自己的函数和内部依赖项,它们是唯一的。例如,对于每个设置子类,都有一个不同的用户界面。你知道吗

感谢您的帮助!你知道吗


Tags: tonameself应用程序信号init部件def
1条回答
网友
1楼 · 发布于 2024-03-28 07:55:18

编辑:问题已经更新,同时,我在答案底部添加了一个更具体的解决方案。你知道吗

我觉得这个问题有点“基于意见”,但既然我也遇到过类似的情况,我想提出我的建议。在这种情况下,重要的是要明白,做事情没有一个好方法,而是有很多错误的方法。你知道吗

原始答案

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

在本例中,每个选项卡“group”都有自己的valueChanged信号,其中包括组名、设置名和值;源信号从“source”(在本例中是spinbox)激发,然后跟随其父级,然后依次“添加”它们的名称。
请记住,您也可以对每个父级使用通用的pyqtSignal(object),并将其与widget.valueChanged.connect(self.valueChanged)连接,然后通过向后遍历self.sender()父级来跟踪其组和设置。你知道吗

最后要注意的是,如果您将这些值用于应用程序设置,请记住Qt已经提供了QSettingsAPI,对于您需要在应用程序中设置的每个配置(以及在会话之间记住的配置),该API可以用作公共的和OS透明的接口。我在示例中实现了它,但我建议您阅读它的文档以更好地理解它的工作原理。你知道吗

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_())

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

由于答案在更新中变得更加具体,我将添加另一个建议。
据我们现在所知,您不需要那种级别的“嵌套”类,而是需要更专门设计的代码,这些代码可以根据您的目的进行重用。另外,由于您使用的是基于二进制的数据,因此只要您知道位操作的工作原理(我假设您知道),并且设置“widgets”不需要特定的GUI定制,就可以使事情变得更简单。你知道吗

在本例中,我只创建了一个“setting”类和一个“group”类,它们的实例只根据它们的名称和默认值创建。你知道吗

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_())

相关问题 更多 >