add 视频显示框、调整布局
This commit is contained in:
@ -10,7 +10,7 @@ from .widgets.hopper_widget import HopperWidget
|
||||
from .widgets.arc_progress_widget import ArcProgressWidget
|
||||
from .widgets.production_progress_widget import ProductionProgressWidget
|
||||
from .widgets.system_button_widget import SystemButtonWidget
|
||||
|
||||
from .widgets.vibration_video_widget import VibrationVideoWidget
|
||||
|
||||
class MainWindow(QWidget):
|
||||
def __init__(self):
|
||||
@ -20,8 +20,6 @@ class MainWindow(QWidget):
|
||||
self.setupLayout() # 设置布局
|
||||
self.connectSignalToSlot()
|
||||
|
||||
|
||||
|
||||
def connectSignalToSlot(self):
|
||||
# 可添加信号槽连接
|
||||
self.system_button_widget.buttons["系统启动"].clicked.connect(self.handleSystemStart)
|
||||
@ -53,6 +51,7 @@ class MainWindow(QWidget):
|
||||
self.arc_progress = ArcProgressWidget() # 中间2:弧形进度部件
|
||||
self.production_progress = ProductionProgressWidget() # 生产进度部件
|
||||
self.system_button_widget = SystemButtonWidget() # 系统控制按钮
|
||||
self.vibration_video = VibrationVideoWidget() # 振捣视频控件
|
||||
|
||||
def setupLayout(self):
|
||||
"""设置垂直布局,从上到下排列部件"""
|
||||
@ -61,12 +60,24 @@ class MainWindow(QWidget):
|
||||
main_layout.setSpacing(0) # 部件间距0px
|
||||
main_layout.setContentsMargins(15, 15, 15, 15) # 上下左右边距15px
|
||||
|
||||
sub_v_layout = QVBoxLayout()
|
||||
sub_v_layout.setSpacing(0)
|
||||
# 依次添加部件到布局(从上到下)
|
||||
main_layout.addWidget(self.status_monitor, alignment=Qt.AlignHCenter)
|
||||
main_layout.addWidget(self.hopper_widget, alignment=Qt.AlignHCenter)
|
||||
main_layout.addWidget(self.arc_progress, alignment=Qt.AlignHCenter)
|
||||
main_layout.addWidget(self.production_progress, alignment=Qt.AlignHCenter)
|
||||
main_layout.addWidget(self.system_button_widget, alignment=Qt.AlignHCenter)
|
||||
# sub_v_layout.addWidget(self.status_monitor, alignment=Qt.AlignHCenter)
|
||||
sub_v_layout.addWidget(self.hopper_widget, alignment=Qt.AlignHCenter)
|
||||
sub_v_layout.addWidget(self.arc_progress, alignment=Qt.AlignHCenter)
|
||||
sub_v_layout.addWidget(self.production_progress, alignment=Qt.AlignHCenter)
|
||||
# sub_v_layout.addWidget(self.system_button_widget, alignment=Qt.AlignHCenter)
|
||||
|
||||
middle_h_layout = QHBoxLayout()
|
||||
middle_h_layout.setSpacing(20)
|
||||
# 加入垂直子布局(设置拉伸因子1,让其占满水平剩余空间)
|
||||
middle_h_layout.addLayout(sub_v_layout, stretch=1)
|
||||
# 加入振捣视频控件(对齐方式为顶部)
|
||||
middle_h_layout.addWidget(self.vibration_video, alignment=Qt.AlignTop)
|
||||
|
||||
# === 添加到著布局
|
||||
main_layout.addLayout(middle_h_layout)
|
||||
|
||||
# 将布局应用到主窗口
|
||||
self.setLayout(main_layout)
|
||||
|
||||
251
view/widgets/vibration_video_widget.py
Normal file
251
view/widgets/vibration_video_widget.py
Normal file
@ -0,0 +1,251 @@
|
||||
# coding:utf-8
|
||||
from PySide6.QtCore import Qt, QTimer, Signal, QObject
|
||||
from PySide6.QtGui import QImage, QPixmap
|
||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QApplication
|
||||
from typing import Optional
|
||||
|
||||
import cv2
|
||||
import time
|
||||
from threading import Thread, Lock
|
||||
import sys
|
||||
|
||||
|
||||
# ====================== 后台视频流管理(自动重连)======================
|
||||
class VideoStream:
|
||||
"""后台读取 RTSP 流,自动重连,只返回新鲜帧"""
|
||||
|
||||
def __init__(self, rtsp_url, name="Stream"):
|
||||
self.rtsp_url = rtsp_url
|
||||
self.name = name
|
||||
self.cap = None
|
||||
self.frame = None
|
||||
self.timestamp = 0
|
||||
self.lock = Lock()
|
||||
self.running = False
|
||||
self.thread = None
|
||||
|
||||
def start(self):
|
||||
self.running = True
|
||||
self.thread = Thread(target=self.update, args=(), daemon=True)
|
||||
self.thread.start()
|
||||
return self
|
||||
|
||||
def update(self):
|
||||
while self.running:
|
||||
if self.cap is None or not self.cap.isOpened():
|
||||
print(f"[{self.name}] 正在连接 RTSP: {self.rtsp_url}")
|
||||
try:
|
||||
self.cap = cv2.VideoCapture(self.rtsp_url, cv2.CAP_FFMPEG)
|
||||
if hasattr(cv2, 'CAP_PROP_BUFFERSIZE'):
|
||||
self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
|
||||
if hasattr(cv2, 'CAP_PROP_READ_TIMEOUT'):
|
||||
self.cap.set(cv2.CAP_PROP_READ_TIMEOUT, 2000)
|
||||
if hasattr(cv2, 'CAP_PROP_TCP_NODELAY'):
|
||||
self.cap.set(cv2.CAP_PROP_TCP_NODELAY, 1)
|
||||
except Exception as e:
|
||||
print(f"[{self.name}] 连接失败: {e}")
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
ret, frame = self.cap.read()
|
||||
if ret:
|
||||
with self.lock:
|
||||
self.frame = frame.copy()
|
||||
self.timestamp = time.time()
|
||||
else:
|
||||
print(f"[{self.name}] 读取失败,准备重连...")
|
||||
if self.cap:
|
||||
self.cap.release()
|
||||
self.cap = None
|
||||
time.sleep(1)
|
||||
|
||||
def read(self):
|
||||
with self.lock:
|
||||
if self.frame is not None and time.time() - self.timestamp < 1.5:
|
||||
return self.frame.copy()
|
||||
else:
|
||||
return None
|
||||
|
||||
def stop(self):
|
||||
self.running = False
|
||||
if self.thread is not None:
|
||||
self.thread.join(timeout=0.5)
|
||||
if self.cap is not None:
|
||||
self.cap.release()
|
||||
print(f"[{self.name}] 视频流已停止")
|
||||
|
||||
|
||||
# ====================== 摄像头功能模块 ======================
|
||||
class CameraModule(QWidget):
|
||||
"""单个摄像头模块:原图显示"""
|
||||
|
||||
def __init__(self, camera_name="摄像头", rtsp_url="", need_rotate_180=True, parent=None):
|
||||
super().__init__(parent)
|
||||
self.camera_name = camera_name
|
||||
self.rtsp_url = rtsp_url
|
||||
self.need_rotate_180 = need_rotate_180 # 画面是否需要旋转180度后显示
|
||||
self.setup_ui()
|
||||
|
||||
def setup_ui(self):
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setSpacing(0)
|
||||
# layout.setContentsMargins(8, 8, 8, 8)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
# --- 0. 标题 ---
|
||||
self.title_label = QLabel()
|
||||
self.title_label.setFixedSize(106, 25)
|
||||
self.title_label.setAlignment(Qt.AlignLeft)
|
||||
self.title_label.setText(f"{self.camera_name}视频")
|
||||
self.title_label.setObjectName("cameraTitleLabel")
|
||||
self.title_label.setStyleSheet("""
|
||||
#cameraTitleLabel {
|
||||
font-size: 16px;
|
||||
color: black;
|
||||
font-weight: bold;
|
||||
}
|
||||
""")
|
||||
|
||||
# --- 1. 原始图像 ---
|
||||
self.raw_label = QLabel() # 显示图像的 label
|
||||
# self.raw_label.setFixedSize(320, 240)
|
||||
self.raw_label.setFixedSize(327, 199) # 显示的图像的宽、高
|
||||
|
||||
palette = self.raw_label.palette()
|
||||
palette.setColor(self.raw_label.foregroundRole(), Qt.white) # 字体颜色:白色
|
||||
self.raw_label.setPalette(palette)
|
||||
|
||||
self.raw_label.setAlignment(Qt.AlignCenter)
|
||||
# self.raw_label.setStyleSheet("background: black; border: 1px solid #ccc;")
|
||||
self.raw_label.setStyleSheet("background: #033474; border: none") # 视频显示框样式
|
||||
self.raw_label.setText(f"{self.camera_name}摄像头, 连接中...")
|
||||
|
||||
# --- 添加到主布局 ---
|
||||
layout.addWidget(self.title_label) # 标题
|
||||
layout.addWidget(self.raw_label) # 图像
|
||||
|
||||
def update_raw_image(self, image: Optional[QImage]):
|
||||
if image:
|
||||
pixmap = QPixmap.fromImage(image)
|
||||
self.raw_label.setPixmap(pixmap.scaled(
|
||||
self.raw_label.size(),
|
||||
Qt.KeepAspectRatio,
|
||||
Qt.SmoothTransformation
|
||||
))
|
||||
self.raw_label.setText("")
|
||||
else:
|
||||
self.raw_label.setPixmap(QPixmap())
|
||||
self.raw_label.setText(f"{self.camera_name}摄像头, 断线中...")
|
||||
|
||||
class VibrationVideoWidget(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent=parent)
|
||||
self.setObjectName("vibrationVideoWidget")
|
||||
|
||||
# 显示摄像头画面的模组
|
||||
# 需要修改为相应的地址!!!
|
||||
self.cam1 = CameraModule("上位料斗", "rtsp://admin:XJ123456@192.168.1.50:554/streaming/channels/101")
|
||||
self.cam2 = CameraModule("下位料斗", "rtsp://admin:XJ123456@192.168.1.51:554/streaming/channels/101")
|
||||
self.cam3 = CameraModule("模具车", "rtsp://admin:XJ123456@192.168.1.51:554/streaming/channels/101")
|
||||
|
||||
self.setup_ui()
|
||||
self.init_streams()
|
||||
self.connect_signals()
|
||||
|
||||
def setup_ui(self):
|
||||
# 视频widget样式
|
||||
self.setFixedSize(387, 720) # 宽387、高792
|
||||
self.setAutoFillBackground(True)
|
||||
self.setStyleSheet("""
|
||||
#vibrationVideoWidget {
|
||||
background-color: #043d76;
|
||||
border: none; /* 可选:去除默认边框,避免视觉干扰 */
|
||||
}
|
||||
""")
|
||||
|
||||
# 布局设置
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(16, 16, 16, 16)
|
||||
layout.setSpacing(6) # 每两个摄像头模块间隔6px
|
||||
layout.addWidget(self.cam1)
|
||||
layout.addWidget(self.cam2)
|
||||
layout.addWidget(self.cam3)
|
||||
|
||||
def init_streams(self):
|
||||
url1 = self.cam1.rtsp_url
|
||||
url2 = self.cam2.rtsp_url
|
||||
url3 = self.cam3.rtsp_url
|
||||
|
||||
self.stream1 = VideoStream(url1, "Cam1").start()
|
||||
self.stream2 = VideoStream(url2, "Cam2").start()
|
||||
self.stream3 = VideoStream(url3, "Cam3").start()
|
||||
|
||||
self.timer1 = QTimer()
|
||||
self.timer1.timeout.connect(self.update_frame1)
|
||||
|
||||
self.timer2 = QTimer()
|
||||
self.timer2.timeout.connect(self.update_frame2)
|
||||
|
||||
self.timer3 = QTimer()
|
||||
self.timer3.timeout.connect(self.update_frame3)
|
||||
|
||||
# 定时读取视频流
|
||||
self.timer1.start(33)
|
||||
self.timer2.start(33)
|
||||
self.timer3.start(33)
|
||||
|
||||
def connect_signals(self):
|
||||
pass
|
||||
|
||||
def update_frame1(self):
|
||||
frame = self.stream1.read()
|
||||
if frame is not None:
|
||||
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
if self.cam1.need_rotate_180:
|
||||
frame_rgb = cv2.rotate(frame_rgb, cv2.ROTATE_180)
|
||||
h, w, ch = frame_rgb.shape
|
||||
q_img = QImage(frame_rgb.data, w, h, ch * w, QImage.Format_RGB888)
|
||||
self.cam1.update_raw_image(q_img)
|
||||
else:
|
||||
self.cam1.update_raw_image(None)
|
||||
|
||||
def update_frame2(self):
|
||||
frame = self.stream2.read()
|
||||
if frame is not None:
|
||||
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
if self.cam2.need_rotate_180:
|
||||
frame_rgb = cv2.rotate(frame_rgb, cv2.ROTATE_180)
|
||||
h, w, ch = frame_rgb.shape
|
||||
q_img = QImage(frame_rgb.data, w, h, ch * w, QImage.Format_RGB888)
|
||||
self.cam2.update_raw_image(q_img)
|
||||
else:
|
||||
self.cam2.update_raw_image(None)
|
||||
|
||||
def update_frame3(self):
|
||||
frame = self.stream3.read()
|
||||
if frame is not None:
|
||||
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||
if self.cam3.need_rotate_180:
|
||||
frame_rgb = cv2.rotate(frame_rgb, cv2.ROTATE_180)
|
||||
h, w, ch = frame_rgb.shape
|
||||
q_img = QImage(frame_rgb.data, w, h, ch * w, QImage.Format_RGB888)
|
||||
self.cam3.update_raw_image(q_img)
|
||||
else:
|
||||
self.cam3.update_raw_image(None)
|
||||
|
||||
# ====================== 清理资源 ======================
|
||||
def closeEvent(self, event):
|
||||
self.hide()
|
||||
self.stream1.stop()
|
||||
self.stream2.stop()
|
||||
self.stream3.stop()
|
||||
self.timer1.stop()
|
||||
self.timer2.stop()
|
||||
self.timer3.stop()
|
||||
super().closeEvent(event)
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication([])
|
||||
window = VibrationVideoWidget()
|
||||
window.show()
|
||||
sys.exit(app.exec())
|
||||
Reference in New Issue
Block a user