首页 > 解决方案 > Pyqt5:尝试添加QLayout“表单”,已经有布局(多继承python)

问题描述

我使用 qtDesigner 创建了一个 ui 文件window.ui(包含一个选项卡小部件)和一个小部件文件student(一些按钮、功能),然后使用 pyuic5 转换为 py 文件。并继承在一个单独的文件中,如mainWindow.pymainStudent.py

我在 mainWindow.py 中添加了一个 tabWidget,我想从 tab 调用页面 student.py 。所以我创建了一个新文件app.py,我首先从mainWindow.py继承类 并添加一个选项卡调用 student 并尝试从mainStudent.py继承类。

我的目标是,如果我运行app.py,那么mainWindo w 将与 tabwidget 一起出现,其中选项卡名称为“学生”,如果我点击学生选项卡,则所有元素都将从“ mainStudent.py ”显示。

但我收到此错误: 尝试将 QLayout“”添加到已经有布局的 studentPage“表单” (注意:函数工作正常)

我不知道我在哪里做错了!请帮忙!

window.py(使用 pyuic5 从 window.ui 生成)

from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(800, 600)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
        self.verticalLayout.setObjectName("verticalLayout")
        self.label = QtWidgets.QLabel(self.centralwidget)
        font = QtGui.QFont()
        font.setPointSize(20)
        self.label.setFont(font)
        self.label.setObjectName("label")
        self.verticalLayout.addWidget(self.label)
        self.tabWidget = QtWidgets.QTabWidget(self.centralwidget)
        self.tabWidget.setObjectName("tabWidget")
        self.verticalLayout.addWidget(self.tabWidget)
        MainWindow.setCentralWidget(self.centralwidget)

        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
        self.label.setText(_translate("MainWindow", "Main Window"))


if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    MainWindow = QtWidgets.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(MainWindow)
    MainWindow.show()
    sys.exit(app.exec_())

student.py(使用 pyuic5 从 window.ui 生成)

from PyQt5 import QtCore, QtGui, QtWidgets

class Ui_Form(object):
    def setupUi(self, Form):
        Form.setObjectName("Form")
        Form.resize(716, 635)
        self.gridLayout_2 = QtWidgets.QGridLayout(Form)
        self.gridLayout_2.setObjectName("gridLayout_2")
        self.label = QtWidgets.QLabel(Form)
        font = QtGui.QFont()
        font.setPointSize(16)
        self.label.setFont(font)
        self.label.setObjectName("label")
        self.gridLayout_2.addWidget(self.label, 0, 0, 1, 1)
        self.tabWidget = QtWidgets.QTabWidget(Form)
        self.tabWidget.setObjectName("tabWidget")
        self.tab = QtWidgets.QWidget()
        self.tab.setObjectName("tab")
        self.gridLayout = QtWidgets.QGridLayout(self.tab)
        self.gridLayout.setObjectName("gridLayout")
        self.pushButton = QtWidgets.QPushButton(self.tab)
        self.pushButton.setObjectName("pushButton")
        self.gridLayout.addWidget(self.pushButton, 0, 0, 1, 1)
        self.tabWidget.addTab(self.tab, "")
        self.tab_2 = QtWidgets.QWidget()
        self.tab_2.setObjectName("tab_2")
        self.gridLayout_3 = QtWidgets.QGridLayout(self.tab_2)
        self.gridLayout_3.setObjectName("gridLayout_3")
        self.pushButton_2 = QtWidgets.QPushButton(self.tab_2)
        self.pushButton_2.setObjectName("pushButton_2")
        self.gridLayout_3.addWidget(self.pushButton_2, 0, 0, 1, 1)
        self.tabWidget.addTab(self.tab_2, "")
        self.gridLayout_2.addWidget(self.tabWidget, 1, 0, 1, 1)

        self.retranslateUi(Form)
        self.tabWidget.setCurrentIndex(0)
        QtCore.QMetaObject.connectSlotsByName(Form)

    def retranslateUi(self, Form):
        _translate = QtCore.QCoreApplication.translate
        Form.setWindowTitle(_translate("Form", "Form"))
        self.label.setText(_translate("Form", "Student Page"))
        self.pushButton.setText(_translate("Form", "Test Function"))
        self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab), _translate("Form", "Regular"))
        self.pushButton_2.setText(_translate("Form", "Test Second Function"))
        self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2), _translate("Form", "Yearly"))


if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    Form = QtWidgets.QWidget()
    ui = Ui_Form()
    ui.setupUi(Form)
    Form.show()
    sys.exit(app.exec_())

主窗口.py

from PyQt5 import QtCore, QtGui, QtWidgets
import sys
from files.main_interfaces.window import Ui_MainWindow
class MainWindow(QtWidgets.QMainWindow,Ui_MainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        self.setupUi(self)

if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    w = MainWindow()
    w.show()
    sys.exit(app.exec_())

主要学生.py

from PyQt5 import QtCore, QtGui, QtWidgets
import sys
from files.main_interfaces.student import Ui_Form
class stdMainWindow(QtWidgets.QWidget,Ui_Form):
    def __init__(self, parent=None):
        super(stdMainWindow, self).__init__(parent)
        self.setupUi(self)

        self.pushButton.clicked.connect(self.function1)

    def function1(self):
        print("function called")

if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    w = stdMainWindow()
    w.show()
    sys.exit(app.exec_())

应用程序.py

from PyQt5 import QtCore, QtGui, QtWidgets
from mainWindow import MainWindow
from mainStudent import stdMainWindow

class studentPage(stdMainWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setupUi(self)


class MainWindow3(MainWindow):
    def __init__(self):
        super().__init__()
        self.setupUi(self)

        # Add tab
        self.studentPage = studentPage()
        self.tabWidget.addTab(self.studentPage, 'Student')

if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    window = MainWindow3()
    window.show()
    sys.exit(app.exec_())

标签: pythonpyqtpyqt5

解决方案


tl;博士

您正在使用不必要的文件级别和子类化,并且您调用setupUi了太多次。
发生该错误是因为您多次尝试重建 GUI,而您不应该这样做。

虽然在多个文件中进行重构是一种很好的做法,但这并不意味着您应该总是这样做。
查看您的代码,这确实没有任何优势。

例如,该mainWindow文件是完全没有必要的:只需使用相同的概念MainWindow在文件中创建类app.py,并将编程逻辑添加到该类

然后,stdMainWindow该类已经拥有自己的 GUI 设置,因此您应该只导入并使用该类,因为另一个子类化是没有意义的。


由于您显然对此仍然很困惑,因此我将尝试更详细地解释 Qt 如何处理 UI 数据。
对于这种情况,我将使用一个简单的 QWidget 表单,它具有垂直布局和一个按钮。我还建议您仔细阅读并深入研究有关使用 Qt Designer的指南,并确保您真正了解其所有内容,因为其中有很多信息必须完全理解。
不要着急:做实验,慢慢阅读代码,并尝试自己理解发生了什么,包括发生的方式原因

使用生成的代码pyuic

单继承方法

您从中得到的pyuic是一个非常基本的 Pythonobject类:它本身没有也不做任何事情:实际上,它没有任何__init__方法,它主要是一个“便利”类,用于将对象“组合”在一个公共实例对象中。

当您使用小部件实例作为参数调用其setupUi方法时,就会发生魔术。

这是 pyuic 生成的输出:

class Ui_Form(object):
    def setupUi(self, Form):
        Form.setObjectName("Form")
        Form.resize(320, 240)
        self.verticalLayout = QtWidgets.QVBoxLayout(Form)
        self.verticalLayout.setObjectName("verticalLayout")
        self.pushButton = QtWidgets.QPushButton(Form)
        self.pushButton.setObjectName("pushButton")
        self.verticalLayout.addWidget(self.pushButton)

        self.retranslateUi(Form)
        QtCore.QMetaObject.connectSlotsByName(Form)

    def retranslateUi(self, Form):
        _translate = QtCore.QCoreApplication.translate
        Form.setWindowTitle(_translate("Form", "Form"))
        self.pushButton.setText(_translate("Form", "PushButton"))

让我们忽略retranslateUiandconnectSlotsByName部分,因为它们对于我们的需求并不重要。

如果我们遵循单继承方法,那么我们应该如何编写实际创建小部件的文件(我用 生成了 ui 文件pyuic5 test.ui -o ui_test.py):

from PyQt5 import QtWidgets
from ui_test import Ui_Form

class MyWidget(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        self.ui = Ui_Form()
        self.ui.setupUi(self)

if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    mywidget = MyWidget()
    mywidget.show()
    sys.exit(app.exec_())

这就是运行上面的文件时会发生的事情:

  • 该文件是主脚本,所以它进入if语句
  • 它创建一个 QApplication 实例(这是创建基于 GUI 的对象所必需的)
  • 它创建一个 的实例MyWidget这意味着以下内容:
    • MyWidget进入其__init__
    • 它创建一个, 从文件中导入的实例Ui_Formui_test.py
    • setupUimywidgetMyWidget 实例)作为参数调用

现在,让我们看看里面发生了什么setupUi

class Ui_Form(object):
    def setupUi(self, Form):
        # "Form" is actually "mywidget" (the instance), so it will set the
        # object name and size for that instance object
        Form.setObjectName("Form")
        Form.resize(320, 240)
        # the above is exactly the same as doing the following, **inside**
        # the __init__ of MyWidget:
        #
        # self.setObjectName("Form")
        # self.resize(320, 240)

        # create a layout with the "mywidget" argument, which automatically
        # sets the layout for that instance; note that the new object is created
        # as an attribute of "self", which in this case is the "self.ui"
        # of "mywidget"
        self.verticalLayout = QtWidgets.QVBoxLayout(Form)
        self.verticalLayout.setObjectName("verticalLayout")

        # create a pushbutton with the "mywidget" as a parent; this is
        # usually not required if you're adding the widget to a layout, as
        # it will take its ownership automatically; as with the layout,
        # the "pushButton" attribute is created for "self.ui"
        self.pushButton = QtWidgets.QPushButton(Form)
        self.pushButton.setObjectName("pushButton")
        # add the button to the layout
        self.verticalLayout.addWidget(self.pushButton)

结果将是布局按钮将是mywidget.ui. self.ui.verticalLayout您可以使用and从小部件类访问它们self.ui.pushButton,或者在它之外使用mywidget.ui.verticalLayoutand访问它们mywidget.ui.pushButton


为了完整起见,让我们完成程序执行步骤:

  • mywidget实例已创建
  • 它现在正在“显示”(但是,在这一点上,它实际上还不可见!)
  • sys.exit被称为app.exec_()参数

对 QApplication的exec()调用(与它的祖先一样,QGuiApplication.exec()and QCoreApplication.exec())正在阻塞:它们将进入自己的事件循环,等待某些事情发生(通常是来自用户的鼠标/键盘交互或其他一些系统事件)并且不会返回直到某些事情使他们结束它(通常,用户关闭最后一个窗口)。所以,只要应用程序处于“运行状态”,sys.exit不会被实际调用。

在应用程序可能等待的事件中,有一些实际上与 GUI 相关,并且在整个过程开始后发生:窗口框架的创建(与系统就字体、分辨率等进行各种级别的通信) ,小部件的实际“绘画”(之后窗口实际显示给用户)和许多其他。

最后,一旦应用程序以某种方式退出,它就会返回它的返回码sys.exit,这最终会退出你的 python 程序。

多重继承方法

使用多重继承时事情并没有太大变化:不同之处在于MyWidget继承自QWidget Ui_Form因此,当setupUi被调用时,“self”将是mywidget实例并且不会有任何self.ui在起作用:只是self

from PyQt5 import QtWidgets
from ui_test import Ui_Form

class MyWidget(QtWidgets.QWidget, Ui_Form):
    def __init__(self):
        super().__init__()
        self.setupUi(self)

# ...

在这种情况下,布局和按钮可以直接访问(self.verticalLayoutself.pushButton),这是因为两者self Form是同一个对象:

    def setupUi(self, Form):
        # "Form" is actually "mywidget", as "self" is
        Form.setObjectName("Form")
        Form.resize(320, 240)
        self.verticalLayout = QtWidgets.QVBoxLayout(Form)
        # ...

这意味着该setupUi函数在技术上也可以以这种方式重写(假设您在self.setupUi() 没有参数的情况下调用):


    # note that there's no argument here besides "self"
    def setupUi(self):
        self.setObjectName("Form")
        self.resize(320, 240)
        self.verticalLayout = QtWidgets.QVBoxLayout(self)
        # ...

这种方法可以帮助我们更好地理解究竟是做什么的setupUi,因为调用该函数就像执行以下操作(请注意,除了 没有其他继承QtWidgets.QWidget):


class MyWidget(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        self.setObjectName("Form")
        self.resize(320, 240)
        self.verticalLayout = QtWidgets.QVBoxLayout(self)
        self.verticalLayout.setObjectName("verticalLayout")
        self.saveFile = QtWidgets.QPushButton(self)
        self.saveFile.setObjectName("saveFile")

现在。这是最常用的方法,因为它比另一种更简单、更直接:您将小部件视为类实例的直接“子”,因此通过“子子”对象访问它们似乎有点不必要喜欢self.ui

但是,这样做有一个缺点:您必须小心对象命名。由于这种方法会自动使用 Designer 中使用的对象名称来设置实例属性名称,因此您必须确保这些名称不会在其他地方使用,这也是因为 Python 访问对象的方式。
例如,如果你有一个命名的函数saveFile并且你也命名了一个按钮saveFile(我显然是在谈论 Designer 对象名称,而不是按钮的标签),你将无法再直接访问该函数,因为 setupUi 将有它覆盖:

from PyQt5 import QtWidgets
from ui_test import Ui_Form

class MyWidget(QtWidgets.QWidget, Ui_Form):
    def __init__(self):
        super().__init__()
        print('What is "saveFile"?', self.saveFile)
        self.setupUi(self)
        print('What is "saveFile"?', self.saveFile)

    def saveFile(self):
        pass

将发生以下情况:

What is "saveFile"? <bound method MyWidget.saveFile of <__main__.MyWidget object at 0xb21cc1dc>>
What is "saveFile"? <PyQt5.QtWidgets.QPushButton object at 0xb21cc26c>

好吧,说实话;您仍然可以访问该方法,但不能以直接的方式:

    # here we are calling the saveFile method with the instance as its argument
    MyWidget.saveFile(self)

但这不是很方便,对吧?


使用uic.loadUi

此方法允许您立即跳过该pyuic步骤,因为它从.ui使用 Designer 创建的 tue 文件动态创建 UI(相对路径始终相对于加载它们的 python 文件)。这可能非常方便,因为如果您在编辑时不总是记得保存或重建 ui 文件,则可能会导致一些错误或不一致。

此方法的行为几乎与多重继承的行为相同,因此您将以与上述相同的方式获取self.verticalLayout和对象。self.pushButton

from PyQt5 import QtWidgets, uic

class MyWidget(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        uic.loadUi('test.ui', self)

setupUi在这种情况下,“self”参数的处理方式与多重继承示例中使用的函数完全相同:函数中的所有self.*对象setupUi都被创建为self/的属性Form。这显然意味着前面解释的相同命名缺陷也适用于这种情况。

不幸的是,这样做还有一个小缺点:有时布局的默认边距会被忽略并设置为 0,无论您在 ui 文件中设置什么。不过,有一个解决方法,您可以阅读更多关于Size of verticalLayout is different in Qt Designer and PyQt program的信息。


最后,还有一些建议:

  • 通常的约定是只对类使用大写名称,对变量和函数使用小写名称;
  • 关于以上几点,也尽量遵循Python 代码的样式指南(又名 PEP 8),尤其是在与他人共享代码时;
  • 所有这些if __name__ == '__main__'语句都是不必要的,因为您可能不会单独运行这些文件;仅在必要时使用这种语句(在您的情况下,仅用于app.py);
  • 如果您在开始时已经导入了import sys这些语句,则不需要调用它;if
  • 不要过度使用粗体样式,因为这会使您的帖子分散注意力并且更难阅读;阅读更多关于使用 markdown、如何格式化代码以及如何提出好问题(包括其相关链接)的信息;

推荐阅读