如何在 PyQt5 中用另一个小部件替换一个小部件

Posted

技术标签:

【中文标题】如何在 PyQt5 中用另一个小部件替换一个小部件【英文标题】:How to replace a widget with another in PyQt5 【发布时间】:2020-08-22 15:47:09 【问题描述】:

在我的 GUI 应用程序中,我正在向用户显示摄像机流。现在的问题是,用户一次只能看到来自一台摄像机的流,并且为了看到来自其他摄像机的流,他必须输入新摄像机的凭据,例如 usernamepasswordcamera IP .

我想使用对话框来执行此操作。我能够做到这一点,但每次都会弹出一个新窗口。我如何使用QStackedLayout 在不同的相机之间切换,但这次我不能使用它,因为相机对象是在运行时创建的。

我只想在按下按钮时出现一个对话框,并且在输入凭据后必须更换相机。

代码:

from PyQt5 import QtCore, QtGui, QtWidgets
from threading import Thread
from collections import deque
from datetime import datetime
import time
import sys
import cv2
import imutils

class CameraWidget(QtWidgets.QWidget):
    """Independent camera feed
    Uses threading to grab IP camera frames in the background

    @param width - Width of the video frame
    @param height - Height of the video frame
    @param stream_link - IP/RTSP/Webcam link
    @param aspect_ratio - Whether to maintain frame aspect ratio or force into fraame
    """

    def __init__(self, username, password, camera_ip, width=0, height=0, stream_link=0, aspect_ratio=False, parent=None, deque_size=1):
        super(CameraWidget, self).__init__(parent)

        # Initialize deque used to store frames read from the stream
        self.deque = deque(maxlen=deque_size)

        # Slight offset is needed since PyQt layouts have a built in padding
        # So add offset to counter the padding 
        self.screen_width = 640
        self.screen_height = 480
        self.maintain_aspect_ratio = aspect_ratio

        self.camera_stream_link = 'rtsp://:@/Streaming/Channels/2'.format(username, password, camera_ip)

        # Flag to check if camera is valid/working
        self.online = False
        self.capture = None
        self.video_frame = QtWidgets.QLabel()

        self.load_network_stream()

        # Start background frame grabbing
        self.get_frame_thread = Thread(target=self.get_frame, args=())
        self.get_frame_thread.daemon = True
        self.get_frame_thread.start()

        # Periodically set video frame to display
        self.timer = QtCore.QTimer()
        self.timer.timeout.connect(self.set_frame)
        self.timer.start(.5)

        print('Started camera: '.format(self.camera_stream_link))

    def load_network_stream(self):
        """Verifies stream link and open new stream if valid"""

        def load_network_stream_thread():
            if self.verify_network_stream(self.camera_stream_link):
                self.capture = cv2.VideoCapture(self.camera_stream_link)
                self.online = True
        self.load_stream_thread = Thread(target=load_network_stream_thread, args=())
        self.load_stream_thread.daemon = True
        self.load_stream_thread.start()

    def verify_network_stream(self, link):
        """Attempts to receive a frame from given link"""

        cap = cv2.VideoCapture(link)
        if not cap.isOpened():
            return False
        cap.release()
        return True

    def get_frame(self):
        """Reads frame, resizes, and converts image to pixmap"""

        while True:
            try:
                if self.capture.isOpened() and self.online:
                    # Read next frame from stream and insert into deque
                    status, frame = self.capture.read()
                    if status:
                        self.deque.append(frame)
                    else:
                        self.capture.release()
                        self.online = False
                else:
                    # Attempt to reconnect
                    print('attempting to reconnect', self.camera_stream_link)
                    self.load_network_stream()
                    self.spin(2)
                self.spin(.001)
            except AttributeError:
                pass

    def spin(self, seconds):
        """Pause for set amount of seconds, replaces time.sleep so program doesnt stall"""

        time_end = time.time() + seconds
        while time.time() < time_end:
            QtWidgets.QApplication.processEvents()

    def set_frame(self):
        """Sets pixmap image to video frame"""

        if not self.online:
            self.spin(1)
            return

        if self.deque and self.online:
            # Grab latest frame
            frame = self.deque[-1]

            # Keep frame aspect ratio
            if self.maintain_aspect_ratio:
                self.frame = imutils.resize(frame, width=self.screen_width)
            # Force resize
            else:
                self.frame = cv2.resize(frame, (self.screen_width, self.screen_height))
                self.frame = cv2.cvtColor(self.frame, cv2.COLOR_BGR2RGB)
                h, w, ch = self.frame.shape
                bytesPerLine = ch * w

            # Convert to pixmap and set to video frame
            self.img = QtGui.QImage(self.frame, w, h, bytesPerLine, QtGui.QImage.Format_RGB888)
            self.pix = QtGui.QPixmap.fromImage(self.img)
            self.video_frame.setPixmap(self.pix)

    def get_video_frame(self):
        return self.video_frame


class MainWindow(QtWidgets.QMainWindow):

    def __init__(self, username, password, camera_ip, parent=None):
        super(MainWindow, self).__init__(parent)

        # Top frame
        self.top_frame = QtWidgets.QFrame()
        self.top_frame.setStyleSheet("background-color: rgb(153, 187, 255)")

        self.camera = CameraWidget(username, password, camera_ip)
        self.top_layout = QtWidgets.QHBoxLayout()
        self.top_layout.addWidget(self.camera.get_video_frame())
        self.top_frame.setLayout(self.top_layout)


        # Bottom frame
        self.btm_frame = QtWidgets.QFrame()
        self.btm_frame.setStyleSheet("background-color: rgb(208, 208, 225)")

        self.button = QtWidgets.QPushButton('Change Camera')
        self.button.clicked.connect(self.onClick)
        self.btm_layout = QtWidgets.QHBoxLayout()
        self.btm_layout.addStretch()
        self.btm_layout.addWidget(self.button)
        self.btm_layout.setContentsMargins(5, 5, 5, 5)
        self.btm_frame.setLayout(self.btm_layout)


        self.widget = QtWidgets.QWidget()
        self.layout = QtWidgets.QVBoxLayout()
        self.layout.addWidget(self.top_frame, 20)
        self.layout.addWidget(self.btm_frame,1)
        self.layout.setContentsMargins(0, 0, 0, 0)
        self.layout.setSpacing(0)
        self.widget.setLayout(self.layout)
        self.setCentralWidget(self.widget)

    def onClick(self):
        """
        I want this function to open a dialog box
        asking user to enter new cameras credentials
        and display it.
        """


if __name__ == '__main__':

    # Create main application window
    app = QtWidgets.QApplication([])
    app.setStyle(QtWidgets.QStyleFactory.create("Fusion"))
    w = MainWindow('admin', 'vaaan@123', '192.168.1.51')
    w.showMaximized()
    sys.exit(app.exec_())

【问题讨论】:

【参考方案1】:

初步回答

这是否接近您的想法?

from PyQt5 import QtCore, QtGui, QtWidgets
from threading import Thread
from collections import deque
from datetime import datetime
import time
import sys
import cv2
import imutils

class CameraWidget(QtWidgets.QWidget):
    # no change
    ...


class ChangeDialog(QtWidgets.QDialog):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, *kwargs)
        QBtn = QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel
        
        buttonBox = QtWidgets.QDialogButtonBox(QBtn)
        buttonBox.accepted.connect(self.accept)
        buttonBox.rejected.connect(self.reject)

        self.layout = QtWidgets.QVBoxLayout()

        self.setLayout(self.layout)

        vlayout = QtWidgets.QVBoxLayout()
        self.usernameEdit = QtWidgets.QLineEdit()
        self.passwordEdit = QtWidgets.QLineEdit()
        self.passwordEdit.setEchoMode(QtWidgets.QLineEdit.Password)
        self.ipAddrEdit = QtWidgets.QLineEdit()
        vlayout.addWidget(self.usernameEdit)
        vlayout.addWidget(self.passwordEdit)
        vlayout.addWidget(self.ipAddrEdit)

        self.layout.addLayout(vlayout)
        self.layout.addWidget(buttonBox)

    @property
    def username(self):
        return self.usernameEdit.text()

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

    @property
    def ipAddress(self):
        return self.ipAddrEdit.text()


class MainWindow(QtWidgets.QMainWindow):

    def __init__(self, username, password, camera_ip, parent=None):
        super(MainWindow, self).__init__(parent)

        # Top frame
        self.top_frame = QtWidgets.QFrame()
        self.top_frame.setStyleSheet("background-color: rgb(153, 187, 255)")

        self.camera = CameraWidget(username, password, camera_ip)
        self.top_layout = QtWidgets.QHBoxLayout()
        self.top_layout.addWidget(self.camera.get_video_frame())
        self.top_frame.setLayout(self.top_layout)


        # Bottom frame
        self.btm_frame = QtWidgets.QFrame()
        self.btm_frame.setStyleSheet("background-color: rgb(208, 208, 225)")

        self.button = QtWidgets.QPushButton('Change Camera')
        self.button.clicked.connect(self.onClick)
        self.btm_layout = QtWidgets.QHBoxLayout()
        self.btm_layout.addStretch()
        self.btm_layout.addWidget(self.button)
        self.btm_layout.setContentsMargins(5, 5, 5, 5)
        self.btm_frame.setLayout(self.btm_layout)


        self.widget = QtWidgets.QWidget()
        self.layout = QtWidgets.QVBoxLayout()
        self.layout.addWidget(self.top_frame, 20)
        self.layout.addWidget(self.btm_frame,1)
        self.layout.setContentsMargins(0, 0, 0, 0)
        self.layout.setSpacing(0)
        self.widget.setLayout(self.layout)
        self.setCentralWidget(self.widget)

        self.changeDialog = ChangeDialog()
        self.changeDialog.accepted.connect(self.changeCamera)


    def changeCamera(self):
        self.camera = CameraWidget(
            self.changeDialog.username,
            self.changeDialog.password,
            self.changeDialog.ipAddress)
        # not sure if this is necessary
        self.top_layout.takeAt(0)
        self.top_layout.addWidget(self.camera.get_video_frame())

    def onClick(self):
        """
        I want this function to open a dialog box
        asking user to enter new cameras credentials
        and display it.
        """
        self.changeDialog.exec()


if __name__ == '__main__':

    # Create main application window
    app = QtWidgets.QApplication([])
    app.setStyle(QtWidgets.QStyleFactory.create("Fusion"))
    w = MainWindow('admin', 'vaaan@123', '192.168.1.51')
    w.showMaximized()
    sys.exit(app.exec_())

如果不能真正看到某些东西,很难判断是否缺少某些东西,但这应该是正确的方向。

回答cmets

关于必填字段

首先这个提议很粗糙。

您应该在每个 QLineEdit 之前添加 QLabels。

然后你应该编写一些验证逻辑。您可以通过删除我放入的默认“确定”按钮并放置您自己的按钮来做到这一点。按下此按钮时,您会检查每个输入 self(对话框)是否有效。

如果是这种情况,您可以致电accept()。否则,您可以在第一个无效的输入上使用setFocus()

显示之前输入的数据

在我的提议中,我创建了一个与 MainWindow 一起存储的对话框。

它永远不会被破坏,因此所有数据仍然存在。当您第二次显示该对话框时,它仍然保留以前的数据。

如果您愿意,可以每次都创建一个新的对话框对象,或者清除所有输入。

【讨论】:

你能把所有这些输入字段都设为必填吗?以及如何在QLineEdit() 中显示以前输入的值?

以上是关于如何在 PyQt5 中用另一个小部件替换一个小部件的主要内容,如果未能解决你的问题,请参考以下文章

使用 PyQt5 在 Qt Designer 中的自定义(升级)小部件中获取另一个小部件的当前值

在 Qt Creator Designer 中用自定义模板小部件替换小部件

如何使小部件随 PyQt5 中的窗口缩放?

从不同类添加的小部件内的 PyQt5 停止计时器

使用按钮 PyQt5 打开一个小部件

如何在pyqt5中为一个小部件制作一个清晰的板子功能