python - Pyqt5:尝试添加QLayout“表单”,已经有布局(多继承python)
问题描述
我使用 qtDesigner 创建了一个 ui 文件window.ui(包含一个选项卡小部件)和一个小部件文件student(一些按钮、功能),然后使用 pyuic5 转换为 py 文件。并继承在一个单独的文件中,如mainWindow.py和mainStudent.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_())
解决方案
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"))
让我们忽略retranslateUi
andconnectSlotsByName
部分,因为它们对于我们的需求并不重要。
如果我们遵循单继承方法,那么我们应该如何编写实际创建小部件的文件(我用 生成了 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_Form
ui_test.py
- 它
setupUi
以mywidget
(MyWidget
实例)作为参数调用
现在,让我们看看里面发生了什么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.verticalLayout
and访问它们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.verticalLayout
和self.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、如何格式化代码以及如何提出好问题(包括其相关链接)的信息;
推荐阅读
- javascript - 是否可以使用 javascript 扩展桌面通知?
- linux - 从查找中排除特定文件
- reactjs - 为什么回调在 React Scheduler 中不是一个数字?
- reactjs - LESS 样式不适用于 react+webpack 应用程序中的反应组件
- bootstrap-4 - 尽管复制了 js 和 css 脚本,但图像轮播不起作用
- sql - SQL 参数化插入查询
- react-native - 运行 react-native run-android 时出错:“无法加载脚本”
- reactjs - React 包没有从节点模块加载
- c# - 如何仅检测最新更改的数据行以拒绝更改?
- scala - 为什么ScalaTest在匹配时默认不强制类型?