首页 > 解决方案 > PyQt,在窗口中创建一个弹出窗口

问题描述

我正在尝试使用 PyQt5 创建一个 GUI,我想添加一个类似于下面显示的弹出窗口(示例取自 Discord,因为这是我能想到的第一件事)。我将如何在我已经拥有的窗口中创建一个弹出窗口。我不想使用消息框之类的东西创建一个全新的窗口,弹出窗口将是静态的(不能在屏幕上手动移动),并且应该能够通过单击关闭/x按钮来关闭,按 esc,或在小部件外部单击

我想要的例子

标签: pythonqtpyqtpyqt5

解决方案


创建“嵌入式”小部件实际上很容易:您唯一需要做的就是在创建它时将其父级设置为窗口(或小部件)。Reparenting 也可以在创建小部件后完成(但始终需要调用show()or )。setVisible(True)

问题是,一旦小部件显示在父级“内部”,仍然可以访问其他小部件。

诀窍是创建一个隐藏级内容的小部件,并在较小的小部件中显示实际内容。为此,您必须在父级上安装事件过滤器,并在调整父级大小时始终重新定位小部件。

然后,为了提供类似于 QDialog 的机制,您可以创建一个事件循环并exec()像普通的 Qt 对话框一样调用它,并允许 ui 元素与之交互。

示例登录屏幕截图

from PyQt5 import QtCore, QtWidgets

class LoginPopup(QtWidgets.QWidget):
    def __init__(self, parent):
        super().__init__(parent)
        self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
        self.setAttribute(QtCore.Qt.WA_StyledBackground)
        self.setAutoFillBackground(True)
        self.setStyleSheet('''
            LoginPopup {
                background: rgba(64, 64, 64, 64);
            }
            QWidget#container {
                border: 2px solid darkGray;
                border-radius: 4px;
                background: rgb(64, 64, 64);
            }
            QWidget#container > QLabel {
                color: white;
            }
            QLabel#title {
                font-size: 20pt;
            }
            QPushButton#close {
                color: white;
                font-weight: bold;
                background: none;
                border: 1px solid gray;
            }
        ''')

        fullLayout = QtWidgets.QVBoxLayout(self)

        self.container = QtWidgets.QWidget(
            autoFillBackground=True, objectName='container')
        fullLayout.addWidget(self.container, alignment=QtCore.Qt.AlignCenter)
        self.container.setSizePolicy(
            QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum)

        buttonSize = self.fontMetrics().height()
        self.closeButton = QtWidgets.QPushButton(
            '×', self.container, objectName='close')
        self.closeButton.setFixedSize(buttonSize, buttonSize)
        self.closeButton.clicked.connect(self.reject)

        layout = QtWidgets.QVBoxLayout(self.container)
        layout.setContentsMargins(
            buttonSize * 2, buttonSize, buttonSize * 2, buttonSize)

        title = QtWidgets.QLabel(
            'Enter an email address', 
            objectName='title', alignment=QtCore.Qt.AlignCenter)
        layout.addWidget(title)

        layout.addWidget(QtWidgets.QLabel('EMAIL'))
        self.emailEdit = QtWidgets.QLineEdit()
        layout.addWidget(self.emailEdit)
        layout.addWidget(QtWidgets.QLabel('PASSWORD'))
        self.passwordEdit = QtWidgets.QLineEdit(
            echoMode=QtWidgets.QLineEdit.Password)
        layout.addWidget(self.passwordEdit)

        buttonBox = QtWidgets.QDialogButtonBox(
            QtWidgets.QDialogButtonBox.Ok|QtWidgets.QDialogButtonBox.Cancel)
        layout.addWidget(buttonBox)
        buttonBox.accepted.connect(self.accept)
        buttonBox.rejected.connect(self.reject)
        self.okButton = buttonBox.button(buttonBox.Ok)
        self.okButton.setEnabled(False)

        self.emailEdit.textChanged.connect(self.checkInput)
        self.passwordEdit.textChanged.connect(self.checkInput)
        self.emailEdit.returnPressed.connect(lambda:
                self.passwordEdit.setFocus())
        self.passwordEdit.returnPressed.connect(self.accept)

        parent.installEventFilter(self)

        self.loop = QtCore.QEventLoop(self)
        self.emailEdit.setFocus()

    def checkInput(self):
        self.okButton.setEnabled(bool(self.email() and self.password()))

    def email(self):
        return self.emailEdit.text()

    def password(self):
        return self.passwordEdit.text()

    def accept(self):
        if self.email() and self.password():
            self.loop.exit(True)

    def reject(self):
        self.loop.exit(False)

    def close(self):
        self.loop.quit()

    def showEvent(self, event):
        self.setGeometry(self.parent().rect())

    def resizeEvent(self, event):
        r = self.closeButton.rect()
        r.moveTopRight(self.container.rect().topRight() + QtCore.QPoint(-5, 5))
        self.closeButton.setGeometry(r)

    def eventFilter(self, source, event):
        if event.type() == event.Resize:
            self.setGeometry(source.rect())
        return super().eventFilter(source, event)

    def exec_(self):
        self.show()
        self._raise()
        res = self.loop.exec_()
        self.hide()
        return res


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        central = QtWidgets.QWidget()
        self.setCentralWidget(central)
        layout = QtWidgets.QVBoxLayout(central)
        button = QtWidgets.QPushButton('LOG IN')
        layout.addWidget(button)
        button.clicked.connect(self.showDialog)
        self.setMinimumSize(640, 480)

    def showDialog(self):
        dialog = LoginPopup(self)
        if dialog.exec_():
            print(dialog.email(), dialog.password())

import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())

注意:QDialog 有自己处理父级的方式,它并不总是允许正确管理调整大小和显示,因此与其尝试解决这些“限制”,不如使用 QWidget 更简单、更安全。


推荐阅读