PyQt,在 window 中创建弹出窗口

PyQt, Creating a popup in the window

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

创建“嵌入式”小部件实际上很简单:您唯一需要做的就是将其 parent 设置为 window(或小部件),当你创建它。 Reparenting 也可以在创建小部件后完成(但对 show()setVisible(True) 的调用是 always requi红色)。

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

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

然后,为了提供类似于 QDialog 的机制,您可以创建一个事件循环并像普通 Qt 对话框一样调用它的 exec(),并允许 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 有自己的方式来处理 parenthood,它并不总是允许正确管理调整大小和显示,所以与其尝试解决这些“限制”,不如使用QWidget更简单更安全。