Я разрабатываю приложение PyQt с 8 страницами. На каждой странице пользователь должен иметь возможность незаметно перейти к другим 7 существующим страницам. «Бесподобный» означает, что при переключении не должно быть вспышки, и новый экран открывается в том же окне. Будучи новичком в этом, я последовал примеру на YouTube, который показался мне хорошим. Это показано с использованием QStackedWidget. Вот пример кода метода переключения экранов:
def gotologin(self):
login = LoginScreen()
widget.addWidget(login)
widget.setCurrentIndex(widget.currentIndex() + 1)
def gotocreate(self):
create = CreateAccScreen()
widget.addWidget(create)
widget.setCurrentIndex(widget.currentIndex() + 1)
И сначала это работало, потому что все классы были в одном модуле, а основной метод внизу выглядел так:
#main
app = QApplication(sys.argv)
welcome = WelcomeScreen()
widget = QStackedWidget()
widget.addWidget(welcome)
widget.setFixedHeight(800)
widget.setFixedWidth(1200)
widget.show()
try:
sys.exit(app.exec_())
except:
print("Exiting")
Однако, поскольку этот файл становится больше, я решил поместить классы для последующих страниц в их собственные файлы. Они находятся в отдельном файле без основного метода, поэтому они не могут ссылаться на «виджет».
Я могу передать ссылку на объект "виджет" конструкторам последующих страниц... но что-то мне подсказывает, что это не лучшая идея. Как я могу изменить текущий виджет из каждого класса? Или предпочтительнее другой подход к структуре приложения?
Обновлено: минимальный воспроизводимый пример ниже:
import sys
from PyQt5 import QtWidgets, QtCore
from PyQt5.QtWidgets import QDialog, QApplication, QStackedWidget
class Page1Screen(QDialog):
def __init__(self):
super(Page1Screen, self).__init__()
self.setupUi(self)
self.treeWidget.itemClicked.connect(self.switch_screens)
def setupUi(self, Dialog):
Dialog.setObjectName("Dialog")
Dialog.resize(571, 508)
self.label = QtWidgets.QLabel(Dialog)
self.label.setGeometry(QtCore.QRect(310, 130, 121, 51))
self.label.setStyleSheet("font: 75 12pt \"MS Shell Dlg 2\";")
self.label.setObjectName("label")
self.treeWidget = QtWidgets.QTreeWidget(Dialog)
self.treeWidget.setGeometry(QtCore.QRect(5, 1, 171, 501))
self.treeWidget.setObjectName("treeWidget")
item_0 = QtWidgets.QTreeWidgetItem(self.treeWidget)
item_0 = QtWidgets.QTreeWidgetItem(self.treeWidget)
item_0 = QtWidgets.QTreeWidgetItem(self.treeWidget)
item_0 = QtWidgets.QTreeWidgetItem(self.treeWidget)
self.retranslateUi(Dialog)
QtCore.QMetaObject.connectSlotsByName(Dialog)
def retranslateUi(self, Dialog):
_translate = QtCore.QCoreApplication.translate
Dialog.setWindowTitle(_translate("Dialog", "Dialog"))
self.label.setText(_translate("Dialog", "Page 1"))
self.treeWidget.headerItem().setText(0, _translate("Dialog", "New Column"))
__sortingEnabled = self.treeWidget.isSortingEnabled()
self.treeWidget.setSortingEnabled(False)
self.treeWidget.topLevelItem(0).setText(0, _translate("Dialog", "PAGE 1"))
self.treeWidget.topLevelItem(1).setText(0, _translate("Dialog", "PAGE 2"))
self.treeWidget.topLevelItem(2).setText(0, _translate("Dialog", "PAGE 3"))
self.treeWidget.topLevelItem(3).setText(0, _translate("Dialog", "PAGE 4"))
self.treeWidget.setSortingEnabled(__sortingEnabled)
def switch_screens(self):
item = self.treeWidget.currentItem()
name = item.text(0)
if name == "PAGE 1":
print("We're already on this page")
elif name == "PAGE 2":
p2 = Page2Screen()
widget.addWidget(p2)
widget.setCurrentIndex(widget.currentIndex() + 1)
elif name == "PAGE 3":
p3 = Page3Screen()
widget.addWidget(p3)
widget.setCurrentIndex(widget.currentIndex() + 1)
elif name == "PAGE 4":
p4 = Page4Screen()
widget.addWidget(p4)
widget.setCurrentIndex(widget.currentIndex() + 1)
class Page2Screen(QDialog):
def __init__(self):
super(Page2Screen, self).__init__()
self.setupUi(self)
self.treeWidget.itemClicked.connect(self.switch_screens)
def setupUi(self, Dialog):
Dialog.setObjectName("Dialog")
Dialog.resize(571, 508)
self.label = QtWidgets.QLabel(Dialog)
self.label.setGeometry(QtCore.QRect(310, 130, 121, 51))
self.label.setStyleSheet("font: 75 12pt \"MS Shell Dlg 2\";")
self.label.setObjectName("label")
self.treeWidget = QtWidgets.QTreeWidget(Dialog)
self.treeWidget.setGeometry(QtCore.QRect(5, 1, 171, 501))
self.treeWidget.setObjectName("treeWidget")
item_0 = QtWidgets.QTreeWidgetItem(self.treeWidget)
item_0 = QtWidgets.QTreeWidgetItem(self.treeWidget)
item_0 = QtWidgets.QTreeWidgetItem(self.treeWidget)
item_0 = QtWidgets.QTreeWidgetItem(self.treeWidget)
self.label_2 = QtWidgets.QLabel(Dialog)
self.label_2.setGeometry(QtCore.QRect(0, 0, 571, 511))
self.label_2.setStyleSheet("background: qlineargradient(spread:pad, x1:0.989, y1:1, x2:0.073, y2:0.119, stop:0 rgba(176, 121, 254, 255), stop:1 rgba(255, 255, 255, 255));")
self.label_2.setText("")
self.label_2.setObjectName("label_2")
self.label_2.raise_()
self.label.raise_()
self.treeWidget.raise_()
self.retranslateUi(Dialog)
QtCore.QMetaObject.connectSlotsByName(Dialog)
def retranslateUi(self, Dialog):
_translate = QtCore.QCoreApplication.translate
Dialog.setWindowTitle(_translate("Dialog", "Dialog"))
self.label.setText(_translate("Dialog", "Page 2"))
self.treeWidget.headerItem().setText(0, _translate("Dialog", "New Column"))
__sortingEnabled = self.treeWidget.isSortingEnabled()
self.treeWidget.setSortingEnabled(False)
self.treeWidget.topLevelItem(0).setText(0, _translate("Dialog", "PAGE 1"))
self.treeWidget.topLevelItem(1).setText(0, _translate("Dialog", "PAGE 2"))
self.treeWidget.topLevelItem(2).setText(0, _translate("Dialog", "PAGE 3"))
self.treeWidget.topLevelItem(3).setText(0, _translate("Dialog", "PAGE 4"))
self.treeWidget.setSortingEnabled(__sortingEnabled)
def switch_screens(self):
item = self.treeWidget.currentItem()
name = item.text(0)
if name == "PAGE 1":
p1 = Page1Screen()
widget.addWidget(p1)
widget.setCurrentIndex(widget.currentIndex() + 1)
elif name == "PAGE 2":
print("We're already on this page")
elif name == "PAGE 3":
p3 = Page3Screen()
widget.addWidget(p3)
widget.setCurrentIndex(widget.currentIndex() + 1)
elif name == "PAGE 4":
p4 = Page4Screen()
widget.addWidget(p4)
widget.setCurrentIndex(widget.currentIndex() + 1)
class Page3Screen(QDialog):
def __init__(self):
super(Page3Screen, self).__init__()
self.setupUi(self)
self.treeWidget.itemClicked.connect(self.switch_screens)
def setupUi(self, Dialog):
Dialog.setObjectName("Dialog")
Dialog.resize(571, 508)
self.label = QtWidgets.QLabel(Dialog)
self.label.setGeometry(QtCore.QRect(310, 130, 121, 51))
self.label.setStyleSheet("font: 75 12pt \"MS Shell Dlg 2\";")
self.label.setObjectName("label")
self.treeWidget = QtWidgets.QTreeWidget(Dialog)
self.treeWidget.setGeometry(QtCore.QRect(5, 1, 171, 501))
self.treeWidget.setObjectName("treeWidget")
item_0 = QtWidgets.QTreeWidgetItem(self.treeWidget)
item_0 = QtWidgets.QTreeWidgetItem(self.treeWidget)
item_0 = QtWidgets.QTreeWidgetItem(self.treeWidget)
item_0 = QtWidgets.QTreeWidgetItem(self.treeWidget)
self.label_2 = QtWidgets.QLabel(Dialog)
self.label_2.setGeometry(QtCore.QRect(0, 0, 571, 511))
self.label_2.setStyleSheet("background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1, stop:0 rgba(255, 0, 0, 255), stop:0.339795 rgba(255, 0, 0, 255), stop:0.339799 rgba(255, 255, 255, 255), stop:0.662444 rgba(255, 255, 255, 255), stop:0.662469 rgba(0, 0, 255, 255), stop:1 rgba(0, 0, 255, 255));")
self.label_2.setText("")
self.label_2.setObjectName("label_2")
self.label_2.raise_()
self.label.raise_()
self.treeWidget.raise_()
self.retranslateUi(Dialog)
QtCore.QMetaObject.connectSlotsByName(Dialog)
def retranslateUi(self, Dialog):
_translate = QtCore.QCoreApplication.translate
Dialog.setWindowTitle(_translate("Dialog", "Dialog"))
self.label.setText(_translate("Dialog", "Page 3"))
self.treeWidget.headerItem().setText(0, _translate("Dialog", "New Column"))
__sortingEnabled = self.treeWidget.isSortingEnabled()
self.treeWidget.setSortingEnabled(False)
self.treeWidget.topLevelItem(0).setText(0, _translate("Dialog", "PAGE 1"))
self.treeWidget.topLevelItem(1).setText(0, _translate("Dialog", "PAGE 2"))
self.treeWidget.topLevelItem(2).setText(0, _translate("Dialog", "PAGE 3"))
self.treeWidget.topLevelItem(3).setText(0, _translate("Dialog", "PAGE 4"))
self.treeWidget.setSortingEnabled(__sortingEnabled)
def switch_screens(self):
item = self.treeWidget.currentItem()
name = item.text(0)
if name == "PAGE 1":
p1 = Page1Screen()
widget.addWidget(p1)
widget.setCurrentIndex(widget.currentIndex() + 1)
elif name == "PAGE 2":
p2 = Page2Screen()
widget.addWidget(p2)
widget.setCurrentIndex(widget.currentIndex() + 1)
elif name == "PAGE 3":
print("We're already on this page")
elif name == "PAGE 4":
p4 = Page4Screen()
widget.addWidget(p4)
widget.setCurrentIndex(widget.currentIndex() + 1)
class Page4Screen(QDialog):
def __init__(self):
super(Page4Screen, self).__init__()
self.setupUi(self)
self.treeWidget.itemClicked.connect(self.switch_screens)
def setupUi(self, Dialog):
Dialog.setObjectName("Dialog")
Dialog.resize(571, 508)
self.label = QtWidgets.QLabel(Dialog)
self.label.setGeometry(QtCore.QRect(310, 130, 121, 51))
self.label.setStyleSheet("font: 75 12pt \"MS Shell Dlg 2\";")
self.label.setObjectName("label")
self.treeWidget = QtWidgets.QTreeWidget(Dialog)
self.treeWidget.setGeometry(QtCore.QRect(5, 1, 171, 501))
self.treeWidget.setObjectName("treeWidget")
item_0 = QtWidgets.QTreeWidgetItem(self.treeWidget)
item_0 = QtWidgets.QTreeWidgetItem(self.treeWidget)
item_0 = QtWidgets.QTreeWidgetItem(self.treeWidget)
item_0 = QtWidgets.QTreeWidgetItem(self.treeWidget)
self.label_2 = QtWidgets.QLabel(Dialog)
self.label_2.setGeometry(QtCore.QRect(0, 0, 571, 511))
self.label_2.setStyleSheet("background-color: qlineargradient(spread:pad, x1:1, y1:1, x2:0.132, y2:0.153682, stop:0 rgba(58, 169, 255, 255), stop:1 rgba(255, 255, 255, 255));")
self.label_2.setText("")
self.label_2.setObjectName("label_2")
self.label_2.raise_()
self.label.raise_()
self.treeWidget.raise_()
self.retranslateUi(Dialog)
QtCore.QMetaObject.connectSlotsByName(Dialog)
def retranslateUi(self, Dialog):
_translate = QtCore.QCoreApplication.translate
Dialog.setWindowTitle(_translate("Dialog", "Dialog"))
self.label.setText(_translate("Dialog", "Page 4"))
self.treeWidget.headerItem().setText(0, _translate("Dialog", "New Column"))
__sortingEnabled = self.treeWidget.isSortingEnabled()
self.treeWidget.setSortingEnabled(False)
self.treeWidget.topLevelItem(0).setText(0, _translate("Dialog", "PAGE 1"))
self.treeWidget.topLevelItem(1).setText(0, _translate("Dialog", "PAGE 2"))
self.treeWidget.topLevelItem(2).setText(0, _translate("Dialog", "PAGE 3"))
self.treeWidget.topLevelItem(3).setText(0, _translate("Dialog", "PAGE 4"))
self.treeWidget.setSortingEnabled(__sortingEnabled)
def switch_screens(self):
item = self.treeWidget.currentItem()
name = item.text(0)
if name == "PAGE 1":
p1 = Page1Screen()
widget.addWidget(p1)
widget.setCurrentIndex(widget.currentIndex() + 1)
elif name == "PAGE 2":
p2 = Page2Screen()
widget.addWidget(p2)
widget.setCurrentIndex(widget.currentIndex() + 1)
elif name == "PAGE 3":
p3 = Page3Screen()
widget.addWidget(p3)
widget.setCurrentIndex(widget.currentIndex() + 1)
elif name == "PAGE 4":
print("We're already on this page")
#main
app = QApplication(sys.argv)
welcome = Page1Screen()
widget = QStackedWidget()
widget.addWidget(welcome)
widget.setFixedHeight(571)
widget.setFixedWidth(508)
widget.show()
try:
sys.exit(app.exec_())
except:
print("Exiting")
Ваши методы gotocreate и gotologin создают новый экземпляр виджета всякий раз, когда они вызываются. Если вы планируете переключаться между двумя экранами, то это не идеальный способ добиться этого.
Если вы беспокоитесь, потому что создали экземпляр объекта, скажем foo = Foo(), и теперь хотите передать ссылку на этот экземпляр другой функции в качестве параметра: do_something('a', 'b', foo, 1, 2, 3), не волнуйтесь, это довольно распространено. На самом деле лучше передать его как параметр, чем использовать глобальную переменную для доступа к ссылке из другой области или модуля.
Если этот учебник принадлежит какой-то Hala, я настоятельно рекомендую вам полностью игнорировать его, так как известно, что он содержит множество ужасных предложений и плохих практик.
@ Александр Какой подход был бы лучше? Тот, который будет держать страницы в одном и том же месте на экране без «мигания» между переключением страниц?
@UnstableAndy, пожалуйста, создайте минимальный воспроизводимый пример вашего текущего кода
@UnstableAndy Дочерний объект никогда не должен вызывать родительскую функцию или принимать глобальную переменную. Кроме того, QStackedWidget работает точно так же, как QTabWidget (который, по сути, встраивает частный QStackedWidget): вы не добавляете постоянно новые вкладки, вы повторно используете существующие. Эти функции gotologin и gotocreate должны быть в основном родительском элементе (который, вероятно, должен быть подклассом QStackedWidget или подклассом QMainWindow с установленным центральным виджетом), и эти функции должны быть либо подключены к button.clicked каждой страницы, либо с помощью пользовательского сигнала.
@Alexander отредактировал сообщение с примером копирования/вставки для запуска.
Почему вы не используете макеты?
В вашем примере каждый раз, когда вы переключаетесь на новую страницу, возникает значительная утечка памяти, которая продолжает увеличиваться. Вы можете убедиться в этом сами, используя такие инструменты, как tracemalloc, чтобы отслеживать объем памяти, который использует ваше приложение.
Например, в верхней части вашего кода рядом с импортом добавьте
import tracemalloc
tracemalloc.start()
А затем в конце каждого из ваших конструкторов PageScreen класса __init__ добавьте print(tracemalloc.get_traced_memory()), затем запустите свой код и переместите начало переключения страниц вперед и назад, и вы увидите, как объем памяти растет и растет с каждым изменением страницы.
Это связано с тем, что каждый раз, когда вы переключаетесь на новую страницу, вы создаете новый экземпляр своего PageScreen, не уничтожая предыдущую копию. Было бы намного эффективнее и стабильнее просто определить все ваши различные страницы один раз и добавить их в stackedWidget в самом первом виджете верхнего уровня, который вы создаете, а затем добавить его и виджет дерева, который вы используете для изменения страниц. окно верхнего уровня. Это также дает дополнительное преимущество, заключающееся в сокращении повторяющегося кода.
Например, ниже я создаю виджет, который ведет себя аналогично вашему примеру, примите его, используя всю информацию, которую я уже указал. Я также использую QWidget вместо QDialog просто потому, что документы Qt описывают QDialog как:
«Диалоговое окно — это окно верхнего уровня, в основном используемое для краткосрочных задач и краткого общения с пользователем».
Кроме того, поскольку все ваши виджеты PageScreen идентичны, за исключением их таблицы стилей, я собираюсь определить только один QWidget для представления всех их, а затем передать таблицу стилей в качестве параметра.
import sys
from PyQt5 import QtWidgets, QtCore
from PyQt5.QtWidgets import QApplication, QStackedWidget, QWidget, QVBoxLayout, QHBoxLayout, QLabel
import tracemalloc
class Window(QWidget):
stylesheets = [ # you can store your stylesheets in a list to easily reuse
"background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1, stop:0 rgba(255, 0, 0, 255), stop:0.339795 rgba(255, 0, 0, 255), stop:0.339799 rgba(255, 255, 255, 255), stop:0.662444 rgba(255, 255, 255, 255), stop:0.662469 rgba(0, 0, 255, 255), stop:1 rgba(0, 0, 255, 255));",
"background-color: qlineargradient(spread:pad, x1:1, y1:1, x2:0.132, y2:0.153682, stop:0 rgba(58, 169, 255, 255), stop:1 rgba(255, 255, 255, 255));",
"background: qlineargradient(spread:pad, x1:0.989, y1:1, x2:0.073, y2:0.119, stop:0 rgba(176, 121, 254, 255), stop:1 rgba(255, 255, 255, 255));",
"font: 75 12pt \"MS Shell Dlg 2\";"
]
def __init__(self, parent=None) -> None:
super().__init__(parent=parent)
layout = QHBoxLayout(self) # use layout managers
self.treeWidget = QtWidgets.QTreeWidget(self)
# The tree widget is created only once in the top level window
self.stacked = QStackedWidget() # same thing for the stacked widget
self.widgets = []
self.treeWidget.setHeaderLabel("New Column")
self.treeWidget.currentItemChanged.connect(self.switch_screens)
for i in range(1, 9):
# create each of the tree items and each of the stack pages
# only once for each page.
QtWidgets.QTreeWidgetItem(self.treeWidget, [f"PAGE {i}"], 0)
self.widgets.append(
PageScreen(self.stylesheets[i % len(self.stylesheets)])
)
self.stacked.addWidget(self.widgets[-1]) # add each page to the stacked widget
self.resize(571,508)
layout.addWidget(self.treeWidget) # add the tree and the stacked widget
layout.addWidget(self.stacked) # to the layout manager
def switch_screens(self):
# Here I am using the the tree items text to identify which page to switch
# to, however there are countless ways to achieve the same thing.
item = self.treeWidget.currentItem()
name = item.text(0) # name = Page 1
index = int(name.split()[1]) # index = 1
self.stacked.setCurrentIndex(index)
print(tracemalloc.get_traced_memory())
class PageScreen(QWidget):
def __init__(self, stylesheet):
# passing the stylesheet in as a parameter allows to reduce a bunch of duplicate code.
super().__init__()
self.setStyleSheet(stylesheet)
self.layout = QVBoxLayout(self) # use a layout manager
self.label = QLabel("Label")
self.layout.addWidget(self.label)
if __name__ == "__main__":
tracemalloc.start()
app = QApplication([])
window = Window()
window.show()
print(tracemalloc.get_traced_memory())
app.exec()
Если вы сделаете то, что я предложил выше, с модулем tracemalloc в своем коде, а затем запустите мой пример, который также распечатывает снимки памяти, вы легко сможете увидеть разницу, как только начнете переключать страницы.
Я знаю, что на самом деле это не дает прямого ответа на ваш вопрос, но при использовании этой стратегии нет необходимости переключать страницу из другого модуля или класса, а также устраняется необходимость в глобальных переменных.
Наконец, если по какой-то причине вы хотите, чтобы каждая страница сложенного виджета имела свою собственную копию treeWidget, все, что вам нужно сделать, это определить treeWidget внутри класса PageScreen, и вместо прямого переключения страницы вы будете посылать сигнал от PageScreen который получает окно верхнего уровня, которое затем запускает сложенный виджет для переключения страниц, поэтому в глобальной переменной все равно не будет необходимости.
Эта конфигурация может выглядеть примерно так.
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
class Window(QStackedWidget):
stylesheets = [ # you can store your stylesheets in a list to easily reuse
"background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:1, stop:0 rgba(255, 0, 0, 255), stop:0.339795 rgba(255, 0, 0, 255), stop:0.339799 rgba(255, 255, 255, 255), stop:0.662444 rgba(255, 255, 255, 255), stop:0.662469 rgba(0, 0, 255, 255), stop:1 rgba(0, 0, 255, 255));",
"background-color: qlineargradient(spread:pad, x1:1, y1:1, x2:0.132, y2:0.153682, stop:0 rgba(58, 169, 255, 255), stop:1 rgba(255, 255, 255, 255));",
"background: qlineargradient(spread:pad, x1:0.989, y1:1, x2:0.073, y2:0.119, stop:0 rgba(176, 121, 254, 255), stop:1 rgba(255, 255, 255, 255));",
"font: \"MS Shell Dlg 2\";"
]
def __init__(self, parent=None) -> None:
super().__init__(parent=parent)
self.widgets = {}
for i in range(9):
page = PageScreen(self.stylesheets[i % len(self.stylesheets)])
self.widgets[i] = page
self.addWidget(page)
page.pageSelected.connect(self.switch_page)
self.resize(571,508)
def switch_page(self, index):
self.setCurrentWidget(self.widgets[index])
self.widgets[index].select_tree_item(index)
class PageScreen(QWidget):
pageSelected = pyqtSignal(int)
def __init__(self, stylesheet):
super().__init__()
self.setStyleSheet(stylesheet)
self.layout = QHBoxLayout(self)
self.label = QLabel("Label")
policy = self.label.sizePolicy()
policy.setHorizontalPolicy(policy.Policy.MinimumExpanding)
self.label.setSizePolicy(policy)
self.treeWidget = QTreeWidget(self)
self.treeWidget.setHeaderLabel("New Column")
for i in range(1, 9):
QTreeWidgetItem(self.treeWidget, [f"PAGE {i}"], 0)
self.treeWidget.itemClicked.connect(self.switch_screens)
self.layout.addWidget(self.treeWidget)
self.layout.addWidget(self.label)
def select_tree_item(self, index):
for i in range(self.treeWidget.topLevelItemCount()):
if self.treeWidget.topLevelItem(i).text(0).endswith(str(index)):
self.treeWidget.setCurrentItem(self.treeWidget.topLevelItem(i))
break
def switch_screens(self, item, column):
text = item.text(column)
index = int(text.split()[1])
self.pageSelected.emit(index)
if __name__ == "__main__":
app = QApplication([])
window = Window()
window.show()
app.exec()
Надеюсь это поможет.
Большое спасибо! Первый пример полезен. Второй вроде не работает. В первом примере стиль виджета дерева не меняется, и он очень отзывчив. Во втором: иногда щелчок не меняет страницу, стиль применяется к виджету дерева, а выделенный элемент дерева не соответствует текущей странице.
@UnstableAndy Я исправил это. второй пример теперь должен выглядеть так же быстро и отзывчиво, как и первый.
Когда вы говорите: «...но что-то мне подсказывает, что это не лучшая идея», почему это плохая идея?