如何在 PyQt5 中用另一个小部件替换一个小部件
Posted
技术标签:
【中文标题】如何在 PyQt5 中用另一个小部件替换一个小部件【英文标题】:How to replace a widget with another in PyQt5 【发布时间】:2020-08-22 15:47:09 【问题描述】:在我的 GUI 应用程序中,我正在向用户显示摄像机流。现在的问题是,用户一次只能看到来自一台摄像机的流,并且为了看到来自其他摄像机的流,他必须输入新摄像机的凭据,例如 username
、password
和 camera 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 中的自定义(升级)小部件中获取另一个小部件的当前值