界面修改以及显示

This commit is contained in:
2025-10-31 18:52:31 +08:00
parent bd0815d0e7
commit 290324b5e4
93 changed files with 17169 additions and 12529 deletions

View File

@ -1,30 +1,43 @@
# coding: utf-8
from typing import List
from PySide6.QtCore import Qt, Signal, QEasingCurve, QUrl, QSize, QTimer
from PySide6.QtGui import QIcon, QDesktopServices, QColor
from PySide6.QtCore import Qt, Signal, QEasingCurve, QUrl, QSize, QTimer, QEvent
from PySide6.QtGui import QIcon, QDesktopServices, QColor, QPalette, QBrush, QImage
from PySide6.QtWidgets import (QApplication, QHBoxLayout, QVBoxLayout,
QFrame, QWidget, QSpacerItem, QSizePolicy)
QFrame, QWidget, QSpacerItem, QSizePolicy, QMainWindow, QLineEdit)
from .widgets.status_monitor_widget import StatusMonitorWidget
# from .widgets.status_monitor_widget import StatusMonitorWidget
from .widgets.system_nav_bar import SystemNavBar
from .widgets.mixer_widget import MixerWidget
from .widgets.conveyor_system_widget import ConveyorSystemWidget
from .widgets.task_widget import TaskWidget
from .widgets.plan_widget import PlanWidget
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.system_button_widget import SystemButtonWidget
from .widgets.vibration_video_widget import VibrationVideoWidget
from .widgets.bottom_control_widget import BottomControlWidget
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.initWindow()
self.createSubWidgets() # 创建子部件
self.initSubWidgets() # 初始化子部件
self.setupLayout() # 设置布局
self.connectSignalToSlot()
# 安装事件过滤器,处理计划方量的 QLineEdit的失去焦点事件
self.installEventFilter(self)
def connectSignalToSlot(self):
# 可添加信号槽连接
self.system_button_widget.buttons["系统启动"].clicked.connect(self.handleSystemStart)
self.system_button_widget.buttons["系统停止"].clicked.connect(self.handleSystemStop)
# self.system_button_widget.buttons["系统启动"].clicked.connect(self.handleSystemStart)
# self.system_button_widget.buttons["系统停止"].clicked.connect(self.handleSystemStop)
pass
self.conveyor_system_widget.left_btn.clicked.connect(self.handleHopperMoveLeft)
self.conveyor_system_widget.right_btn.clicked.connect(self.handleHopperMoveRight)
def handleSystemStart(self):
# 测试
@ -39,57 +52,231 @@ class MainWindow(QWidget):
def initWindow(self):
"""初始化窗口基本属性"""
self.setWindowTitle("中交三航主界面") # 设置窗口标题
self.setMinimumSize(1280, 1040) # 设置最小尺寸(可根据需求调整)
# 设置最小尺寸(可根据需求调整)
# 工控机尺寸为 1280*1024
self.setMinimumSize(1280, 1024)
self.setObjectName("MainWindow")
# 设置主界面窗口背景色
self.setStyleSheet("background-color: #ffffff;")
# self.setStyleSheet("background-color: #ffffff;") # #001558
# Qt.FramelessWindowHint
self.setWindowFlags(Qt.FramelessWindowHint)
# 设置主界面背景图片
try:
self.background_image = QImage(":/icons/images/主界面背景.png")
if self.background_image.isNull():
raise Exception("图片为空,可能路径错误或格式不支持")
# print("图片加载成功")
except Exception as e:
print(f"主界面背景图片加载失败: {e}")
self.background_image = None
return # 加载背景失败
self.update_background()
def createSubWidgets(self):
"""创建所有子部件实例"""
self.status_monitor = StatusMonitorWidget() # 最上方:状态监控部件
self.system_nav_bar = SystemNavBar() # 最上方:系统导航栏
self.mixer_widget = MixerWidget(self) # 左侧: 搅拌机
self.conveyor_system_widget = ConveyorSystemWidget(self) # 左侧: 传送带系统
self.segment_task_widget = TaskWidget("管片任务", self) # 左侧:管片任务
self.dispatch_task_widget = TaskWidget("派单任务", self) # 右侧:派单任务
self.plan_table_widget = PlanWidget(self) # 右侧: 计划表单
# self.status_monitor = StatusMonitorWidget() # 状态监控部件
self.hopper_widget = HopperWidget() # 中间1料斗部件
self.arc_progress = ArcProgressWidget() # 中间2弧形进度部件
self.production_progress = ProductionProgressWidget() # 生产进度部件
self.system_button_widget = SystemButtonWidget() # 系统控制按钮
self.vibration_video = VibrationVideoWidget() # 振捣视频控件
self.production_progress = ProductionProgressWidget() # 中间3: 生产进度部件
# self.system_button_widget = SystemButtonWidget() # 系统控制按钮
self.vibration_video = VibrationVideoWidget() # 振捣视频控件 (右侧)
self.bottom_control_widget = BottomControlWidget() # 最下方: 控制的按钮 (系统诊断、系统中心等)
def initSubWidgets(self):
self.dispatch_task_widget.set_task_id("task1", "PD0001")
self.dispatch_task_widget.set_task_id("task2", "PD0002")
self.dispatch_task_widget.set_task_id("task3", "PD0003")
from busisness.blls import ArtifactBll, PDRecordBll
artifact_dal = ArtifactBll()
artifacts = artifact_dal.get_artifact_task()
print("\n打印artifacts数据:")
for i, artifact in enumerate(artifacts):
# 如果是数据类对象,转换为字典输出
# print(artifact.MouldCode)
# if hasattr(artifact, "__dataclass_fields__"):
# print(f"第{i+1}条: {artifact.__dict__}")
# else:
# print(f"第{i+1}条: {artifact}")
self.segment_task_widget.set_task_id(f"task{i + 1}", artifact.MouldCode)
self.segment_task_widget.set_task_volume(f"task{i + 1}", artifact.BetonVolume)
pass
pdrecord_dal = PDRecordBll()
pdrecords = pdrecord_dal.get_PD_record()
# print(pdrecords[0].MouldCode)
print("\n打印pdrecords数据:")
for i, record in enumerate(pdrecords):
# 如果是数据类对象,转换为字典输出
# print(record.__dict__["MouldCode"])
# if hasattr(record, "__dataclass_fields__"):
# print(f"第{i+1}条: {record.__dict__}")
# else:
# print(f"第{i+1}条: {record}")
self.dispatch_task_widget.set_task_volume(f"task{i + 1}", record.BetonVolume)
pass
def setupLayout(self):
"""设置垂直布局,从上到下排列部件"""
main_layout = QVBoxLayout(self) # 主布局:垂直布局
# 设置布局间距(部件之间的距离)和边距(布局与窗口边缘的距离)
main_layout.setSpacing(0) # 部件间距0px
main_layout.setContentsMargins(15, 15, 15, 15) # 上下左右边距15px
main_layout.setContentsMargins(0, 0, 0, 0) # 左上右下边距0px
# 添加最上方的 系统导航栏包括系统标题、中交三航的logo等
main_layout.addWidget(self.system_nav_bar, alignment=Qt.AlignTop)
# 相应控件的坐标设置
self.mixer_widget.move(0, 70) # 搅拌楼坐标
# self.conveyor_system_widget.move(0, 191)
self.conveyor_system_widget.move(0, 171) # 传送带系统坐标
# self.segment_task_widget.move(54, 406)
self.segment_task_widget.move(54, 382) # 管片任务坐标
# self.dispatch_task_widget.move(629, 384)
self.update_dispatch_task_position() # 更新派单任务坐标
self.update_plan_table_position() # 更新计划表单坐标
# 中间的垂直子布局
sub_v_layout = QVBoxLayout()
sub_v_layout.setSpacing(0)
# 依次添加部件到布局(从上到下)
# 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.addSpacerItem(QSpacerItem(
20, 40, # 最小宽高
QSizePolicy.Minimum,
QSizePolicy.Expanding # 垂直方向伸缩,占据剩余空间
))
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.addSpacerItem(QSpacerItem(
20, 40, # 最小宽高
QSizePolicy.Minimum,
QSizePolicy.Expanding # 垂直方向伸缩,占据剩余空间
))
# sub_v_layout.addWidget(self.system_button_widget, alignment=Qt.AlignHCenter)
# 中间的水平布局
middle_h_layout = QHBoxLayout()
middle_h_layout.setSpacing(20)
# 加入垂直子布局设置拉伸因子1让其占满水平剩余空间
# middle_h_layout.setSpacing(20)
middle_h_layout.setSpacing(0)
# 1、加入垂直子布局设置拉伸因子1让其占满水平剩余空间
middle_h_layout.addLayout(sub_v_layout, stretch=1)
# 加入振捣视频控件(对齐方式为顶部)
middle_h_layout.addWidget(self.vibration_video, alignment=Qt.AlignTop)
# 2、加入振捣视频控件
middle_h_layout.addWidget(self.vibration_video, alignment=Qt.AlignCenter)
# === 添加到布局
# === 中间子布局 添加到布局 ===
main_layout.addLayout(middle_h_layout)
# 添加最下方的 控制按钮控件
main_layout.addWidget(self.bottom_control_widget, alignment=Qt.AlignBottom)
# 将布局应用到主窗口
self.setLayout(main_layout)
# 上料斗左移
def handleHopperMoveLeft(self):
# 演示效果
self.hopper_widget.hideUpperHopper()
self.conveyor_system_widget.showHopper() # 开始向左移动,显示料斗 (此时,料斗一定在传送带上的过渡位置)
# 按钮状态:点击料斗左移按钮后,禁用料斗右移按钮
self.conveyor_system_widget.right_btn.setEnabled(False)
# 以下为模拟:
# 假设两秒种之后,移动到了搅拌机下 (这里需要根据实际情况修改)
QTimer.singleShot(2000, self.conveyor_system_widget.moveHopperBelowMixer)
# 料斗左移完成,恢复料斗右移按钮
QTimer.singleShot(2100, lambda: self.conveyor_system_widget.right_btn.setEnabled(True))
# 上料斗右移
def handleHopperMoveRight(self):
# 演示效果
self.conveyor_system_widget.moveHopperToTransition() # 移动到过渡的位置
self.hopper_widget.upper_clamp_widget.set_angle(0) # 上料斗向右移动到目的地时夹爪的角度一定是0
# 按钮状态:点击料斗右移按钮后,禁用料斗左移按钮
self.conveyor_system_widget.left_btn.setEnabled(False)
# 以下为模拟:
# 假设两秒后,传送带中 料斗向右移动完成 (这里需要根据实际情况修改)
QTimer.singleShot(1900, self.conveyor_system_widget.hideHopper) # 料斗向右移动完成,隐藏料斗
QTimer.singleShot(2000, self.hopper_widget.showUpperHopper)
# 料斗右移完成,恢复料斗左移按钮
QTimer.singleShot(2100, lambda: self.conveyor_system_widget.left_btn.setEnabled(True))
# 更新 派单任务widget的坐标
def update_dispatch_task_position(self):
# 方法1获取模具车控件左上角坐标相对于父控件
arc_pos = self.arc_progress.pos()
# print(f"料斗控件左上角坐标相对父控件x={arc_pos.x()}, y={arc_pos.y()}")
# x+462, y-249
self.dispatch_task_widget.move(arc_pos.x()+462, arc_pos.y()-249)
# print("update_dispatch_task_position", arc_pos.x()+462, arc_pos.y()-241)
# 更新 计划表单widget的坐标
def update_plan_table_position(self):
# 方法1获取料斗控件左上角坐标相对于父控件
arc_pos = self.hopper_widget.pos()
# print(f"料斗控件左上角坐标相对父控件x={arc_pos.x()}, y={arc_pos.y()}")
# x+462, y-249
self.plan_table_widget.move(arc_pos.x()+362, arc_pos.y()+40)
def update_background(self):
"""更新主界面背景图片"""
if self.background_image and not self.background_image.isNull():
palette = self.palette()
# 按当前窗口尺寸缩放图片
scaled_image = self.background_image.scaled(
self.size(),
Qt.IgnoreAspectRatio,
Qt.SmoothTransformation
)
palette.setBrush(QPalette.Window, QBrush(scaled_image))
self.setPalette(palette)
self.setAutoFillBackground(True)
def eventFilter(self, watched, event):
"""过滤鼠标点击事件, 让计划方量的QLineEdit在鼠标点击外部时失去焦点"""
# 只处理鼠标左键点击事件
if event.type() == QEvent.MouseButtonPress and event.button() == Qt.LeftButton:
# 获取当前拥有焦点的控件
focus_widget = QApplication.focusWidget()
# 判断焦点是否在QLineEdit上
if isinstance(focus_widget, QLineEdit):
# 将全局鼠标位置转换为焦点控件QLineEdit的局部坐标
local_pos = focus_widget.mapFromGlobal(event.globalPos())
# 若点击位置不在QLineEdit内部则清除焦点
if not focus_widget.rect().contains(local_pos):
focus_widget.clearFocus()
# 将焦点转移到主窗口,避免无焦点状态
self.setFocus()
return super().eventFilter(watched, event)
def resizeEvent(self, e):
"""窗口大小变化时的回调"""
super().resizeEvent(e)
self.update_background() # 重新加载背景图片
self.update_dispatch_task_position() # 更新 派单任务的坐标
self.update_plan_table_position() # 更新计划表单坐标
def closeEvent(self, e):
"""窗口关闭时的回调"""
super().closeEvent(e)
def keyPressEvent(self, event):
if event.key() == Qt.Key_Escape: # 按下Esc键, 退出界面
self.close()
super().keyPressEvent(event)
if __name__ == "__main__":
import sys

View File

@ -2,7 +2,7 @@ from PySide6.QtWidgets import QApplication, QMainWindow, QLabel, QWidget, QVBoxL
from PySide6.QtGui import QPixmap
from PySide6.QtCore import Qt, QPropertyAnimation, Property
import os
import resources.resource_rc
import resources.resources_rc
# 拱形进度条
class OverlapArcProgress(QWidget):
@ -45,18 +45,18 @@ class OverlapArcProgress(QWidget):
self.fg_label.setGeometry(0, self.total_height, self.bg_pixmap.width(), 0)
# ---------- 进度显示标签 ----------
self.progress_label = QLabel(self)
self.progress_label.setGeometry(217, 17, 121, 34)
self.progress_label.setStyleSheet("""
background-color: #444750;
color: #A2EF4D;
font-size: 20px;
font-weight: bold;
""")
# 文本在标签内部居中显示
self.progress_label.setAlignment(Qt.AlignCenter)
self.progress_label.raise_() # 确保在前景上层(最上层)
self.progress_label.setText("0%") # 初始文本
# self.progress_label = QLabel(self)
# self.progress_label.setGeometry(217, 17, 121, 34)
# self.progress_label.setStyleSheet("""
# background-color: #444750;
# color: #A2EF4D;
# font-size: 20px;
# font-weight: bold;
# """)
# # 文本在标签内部居中显示
# self.progress_label.setAlignment(Qt.AlignCenter)
# self.progress_label.raise_() # 确保在前景上层(最上层)
# self.progress_label.setText("0%") # 初始文本
# ---------- 进度属性 ----------
@Property(int)
@ -73,7 +73,7 @@ class OverlapArcProgress(QWidget):
def update_foreground(self):
"""通过调整前景标签的高度和Y坐标, 实现从底部向上填充的进度条"""
# 同步更新进度标签文本
self.progress_label.setText(f"{self._progress}%")
# self.progress_label.setText(f"{self._progress}%")
# print(f"{self._progress}%")
progress_ratio = self._progress / 100.0
@ -105,90 +105,132 @@ class OverlapArcProgress(QWidget):
class ArcProgressWidget(QWidget):
def __init__(self):
super().__init__()
# 加载MainWidget背景图
# 加载ArcProgressWidget背景图
# 添加了 拱3.png 作为背景图
bg_img = ":/icons/images/拱3.png" # 需要修改为实际的图片的路径
# self.setStyleSheet(f"background-image: url({bg_img}); background-repeat: no-repeat; background-position: center;")
self.bg_pixmap = QPixmap(bg_img)
# self.setStyleSheet("background-color: red;")
# # 设置控件widget大小与背景图一致
self.setFixedSize(self.bg_pixmap.size())
# self.bg_pixmap.size() 为 555x227
# self.setFixedSize(self.bg_pixmap.size())
# self.setFixedSize(555, 254)
self.setFixedSize(555, 259)
self.setSizePolicy(
QSizePolicy.Fixed, # 水平方向固定
QSizePolicy.Fixed # 垂直方向固定
)
# 创建背景标签
background_label = QLabel(self)
# 创建垂直主布局
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 0) # 去除边距
main_layout.setSpacing(0) # 去除控件间距
# 1. 添加标题标签(保持居中)
self.arc_title_label = QLabel("振捣模具车", self)
self.arc_title_label.setFixedSize(555, 29)
self.arc_title_label.setAlignment(Qt.AlignCenter)
self.arc_title_label.setStyleSheet("""
color: #0bffff;
font-size: 18px;
background-image: url(:/icons/images/文字标题底.png);
background-repeat: no-repeat;
background-position: center;
margin: 4px auto;
""")
main_layout.addWidget(self.arc_title_label, alignment=Qt.AlignCenter)
# 2. 创建普通QWidget作为容器大小与背景图一致
container = QWidget(self)
container.setFixedSize(self.bg_pixmap.size()) # 容器大小=背景图大小
# 容器内设置背景图
background_label = QLabel(container)
background_label.setPixmap(self.bg_pixmap)
background_label.setScaledContents(True)
background_label.setFixedSize(self.bg_pixmap.size())
background_label.setFixedSize(container.size())
background_label.lower() # 背景置底,避免遮挡其他控件
# 3. 在容器内添加所有标签和拱形进度条用setGeometry定位
# 创建拱形进度条控件
arc_bg_img = ":/icons/images/拱2.png" # 拱进度条背景(空心) # 需要修改为实际的图片的路径
arc_fg_img = ":/icons/images/拱1.png" # 拱进度条前景(实心) # 需要修改为实际的图片的路径
self.arc_progress = OverlapArcProgress(arc_bg_img, arc_fg_img, self)
# 显示重量的标签,单位是 kg
self.weight_label = QLabel(self)
self.weight_label.setGeometry(217, 118, 118, 29)
arc_bg_img = ":/icons/images/拱2.png" # 替换为实际路径
arc_fg_img = ":/icons/images/拱1.png" # 替换为实际路径
self.arc_progress = OverlapArcProgress(arc_bg_img, arc_fg_img, container)
# 显示环号的标签
self.ring_number_label = QLabel("环号: 1", container)
self.ring_number_label.setGeometry(98, 118, 116, 29) # 保持原坐标
self.ring_number_label.setStyleSheet("""
background-color: #143a6e;
color: #16ffff;
font-size: 18px;
""")
self.ring_number_label.setAlignment(Qt.AlignCenter)
# 显示重量的标签
self.weight_label = QLabel("2000kg", container)
self.weight_label.setGeometry(217, 118, 118, 29) # 保持原坐标
self.weight_label.setStyleSheet("""
background-color: #3a2528;
color: #A2EF4D;
font-size: 20px;
background-color: #143a6e;
color: #16ffff;
font-size: 18px;
""")
# 文本在标签内部居中显示
self.weight_label.setAlignment(Qt.AlignCenter)
self.weight_label.setText("2000kg") # 初始文本
# 显示管片型号的标签 如 B1
self.segment_model_label = QLabel(self)
self.segment_model_label.setGeometry(98, 150, 116, 29)
# 显示管片型号的标签
self.segment_model_label = QLabel("中埋: R12", container)
self.segment_model_label.setGeometry(98, 150, 116, 29)
self.segment_model_label.setStyleSheet("""
background-color: #3a2528;
color: #A2EF4D;
font-size: 20px;
background-color: #143a6e;
color: #16ffff;
font-size: 18px;
""")
# 文本在标签内部居中显示
self.segment_model_label.setAlignment(Qt.AlignCenter)
self.segment_model_label.setText("B1") # 初始文本
# 显示频率的标签,单位是 Hz
self.frequency_label = QLabel(self)
self.frequency_label.setGeometry(217, 150, 118, 29)
# 显示频率的标签
self.frequency_label = QLabel("210Hz", container)
self.frequency_label.setGeometry(217, 150, 118, 29)
self.frequency_label.setStyleSheet("""
background-color: #3a2528;
color: #A2EF4D;
font-size: 20px;
background-color: #143a6e;
color: #16ffff;
font-size: 18px;
""")
# 文本在标签内部居中显示
self.frequency_label.setAlignment(Qt.AlignCenter)
self.frequency_label.setText("230Hz") # 初始文本
# 显示管片编号的标签 如 SHRB1-3
self.segment_number_label = QLabel(self)
self.segment_number_label.setGeometry(338, 150, 116, 29)
# 显示管片编号的标签
self.segment_number_label = QLabel("SHRB1-3", container)
self.segment_number_label.setGeometry(338, 150, 116, 29)
self.segment_number_label.setStyleSheet("""
background-color: #3a2528;
color: #A2EF4D;
font-size: 20px;
background-color: #143a6e;
color: #16ffff;
font-size: 18px;
""")
# 文本在标签内部居中显示
self.segment_number_label.setAlignment(Qt.AlignCenter)
self.segment_number_label.setText("SHRB1-3") # 初始文本
# 显示状态的标签 (如:振动中等)
self.state_label = QLabel(self)
self.state_label.setGeometry(217, 182, 118, 29)
self.state_label.setStyleSheet("""
background-color: #3a2528;
color: #A2EF4D;
font-size: 20px;
# 显示管片尺寸的标签
self.segment_size_label = QLabel("6900*1500", container)
self.segment_size_label.setGeometry(98, 182, 116, 29)
self.segment_size_label.setStyleSheet("""
background-color: #143a6e;
color: #16ffff;
font-size: 18px;
""")
self.segment_size_label.setAlignment(Qt.AlignCenter)
# 显示状态的标签
self.state_label = QLabel("振动中", container)
self.state_label.setGeometry(217, 182, 118, 29)
self.state_label.setStyleSheet("""
background-color: #143a6e;
color: #16ffff;
font-size: 18px;
""")
# 文本在标签内部居中显示
self.state_label.setAlignment(Qt.AlignCenter)
self.state_label.setText("振动中") # 初始文本
# 4. 将拱形进度条容器添加到主布局
main_layout.addWidget(container, alignment=Qt.AlignCenter)
# 进度条测试
def testProgress(self, seconds: float):
@ -220,7 +262,7 @@ class ArcProgressWidget(QWidget):
def setState(self, stateStr:str):
self.state_label.setText(stateStr)
# 管片类型设置 (B1、B2等)
# 管片类型设置 (B1、B2、中埋: R12 等)
def setSegmentModel(self, segmentModelStr:str):
self.segment_model_label.setText(segmentModelStr)
@ -234,6 +276,6 @@ if __name__ == "__main__":
app = QApplication(sys.argv)
window = ArcProgressWidget()
window.testProgress(120)
window.setState("测试中")
# window.setState("测试中")
window.show()
sys.exit(app.exec())

View File

@ -0,0 +1,291 @@
from PySide6.QtWidgets import (QWidget, QHBoxLayout, QLabel, QPushButton,
QMessageBox, QSpacerItem, QSizePolicy)
from PySide6.QtGui import QPixmap, QPainter, QFont, QPen
from PySide6.QtCore import Qt, QDateTime, QEvent, QSize
from view.widgets.switch_button import SwitchButton
import resources.resources_rc
class BottomControlWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.initUI()
def initUI(self):
# 1. 加载背景图并设置控件尺寸
bg_path = ":/icons/images/底部背景.png"
self.bg_pixmap = QPixmap(bg_path)
if self.bg_pixmap.isNull():
print("警告:底部背景.png 加载失败,请检查路径!")
self.setFixedSize(1280, 66)
else:
self.setFixedSize(self.bg_pixmap.size())
# 2. 主布局水平布局组件间距6px
main_layout = QHBoxLayout(self)
main_layout.setContentsMargins(10, 10, 10, 10) # 适当留白避免贴边
main_layout.setSpacing(6)
# 3. 逐个添加组件
## 3.1 系统诊断按钮(带状态图)
self.diagnosis_btn = self.createDiagnosisButton("系统诊断",
":/icons/images/底部短按钮1.png",
":/icons/images/底部短按钮2.png")
main_layout.addWidget(self.diagnosis_btn)
## 3.2 系统状态消息按钮(长按钮)
self.status_msg_btn = self.createLongButton("系统状态消息",
":/icons/images/底部长按钮1.png",
":/icons/images/底部长按钮2.png")
main_layout.addWidget(self.status_msg_btn)
## 3.3 预警消息列表按钮(长按钮)
self.warning_list_btn = self.createLongButton("预警消息列表",
":/icons/images/底部长按钮1.png",
":/icons/images/底部长按钮2.png")
main_layout.addWidget(self.warning_list_btn)
## 3.4 系统中心按钮(短按钮,无状态图)
self.center_btn = self.createShortButton("系统中心",
":/icons/images/底部短按钮1.png",
":/icons/images/底部短按钮2.png")
main_layout.addWidget(self.center_btn)
## 3.5 自动模式(文字+开关)
self.auto_mode_widget = self.createAutoModeWidget()
main_layout.addWidget(self.auto_mode_widget)
## 3.6 版本信息标签(可点击弹框)
self.version_label = QLabel("V1.0")
self.version_label.setStyleSheet("""
color: #16ffff;
font-size: 20px;
text-decoration: underline;
""")
self.version_label.installEventFilter(self) # 安装事件过滤器
main_layout.addWidget(self.version_label, alignment=Qt.AlignRight | Qt.AlignBottom)
def eventFilter(self, obj, event):
"""事件过滤器使用QEvent枚举判断事件类型"""
if obj == self.version_label and event.type() == QEvent.MouseButtonPress:
self.showVersionInfo()
return True # 拦截事件
return super().eventFilter(obj, event) # 其他事件正常处理
def createDiagnosisButton(self, text, normal_img, hover_img):
"""创建带状态图的系统诊断按钮"""
# 加载短按钮图片获取尺寸
normal_pix = QPixmap(normal_img)
if normal_pix.isNull():
print(f"警告:{normal_img} 加载失败")
btn_width, btn_height = 120, 40
else:
btn_width, btn_height = normal_pix.width(), normal_pix.height()
btn = QPushButton()
btn.setFixedSize(btn_width, btn_height)
# 1. 按钮设置文字
btn.setText(text)
# 2. 状态图标(作为按钮的子控件,单独定位)
self.status_icon = QLabel(btn)
status_pix = QPixmap(":/icons/images/底部系统状态绿.png")
if not status_pix.isNull():
self.status_icon.setFixedSize(status_pix.width(), status_pix.height())
self.status_icon.setPixmap(status_pix)
# 图标定位左侧9px垂直居中
icon_y = (btn_height - self.status_icon.height()) // 2
self.status_icon.move(9, icon_y)
# 3. 设置鼠标手势
btn.setCursor(Qt.PointingHandCursor)
# 4. 按钮样式表
btn.setStyleSheet(f"""
QPushButton {{
background-image: url({normal_img});
background-repeat: no-repeat;
background-position: center;
border: none;
color: #3bfff8;
font-size: 20px;
text-align: center;
padding-left: 9px;
}}
QPushButton:hover {{
background-image: url({hover_img});
color: #001c83; /* hover状态文字颜色 */
}}
""")
return btn
def setStatusIcon(self, status, count=0):
"""切换状态图标并显示数量"""
icon_map = {
"normal": ":/icons/images/底部系统状态绿.png",
"warning": ":/icons/images/底部系统状态黄.png",
"error": ":/icons/images/底部系统状态红.png"
}
pixmap_path = icon_map.get(status, icon_map["normal"])
pixmap = QPixmap(pixmap_path)
if not pixmap.isNull():
temp_pix = pixmap.copy() # 复制一份 pixmap 用于绘制
# 绘制 异常或警告 数量若count>0
if count > 0 and status != "normal": # 绿色的正常情况,不需要显示数字
painter = QPainter(temp_pix) # 在副本上绘制
painter.setPen(QPen(Qt.white, 2)) # 白色画笔
font = painter.font() # 获取当前字体
# 调整字体大小
if count > 99:
font.setPointSize(10)
else:
font.setPointSize(14)
painter.setFont(font) # 应用字体设置
if count <= 99: # 两位数字
painter.drawText(temp_pix.rect(), Qt.AlignCenter, str(count)) # 居中绘制数字
else:
painter.drawText(temp_pix.rect(), Qt.AlignCenter, "99+")
painter.end() # 手动结束绘制
# 将绘制完成的副本设置回标签
self.status_icon.setPixmap(temp_pix)
def createShortButton(self, text, normal_img, hover_img):
"""创建短按钮"""
# 以正常状态图片尺寸为准,确定按钮宽高
normal_pix = QPixmap(normal_img)
if normal_pix.isNull():
print(f"警告:{normal_img} 加载失败")
btn_width, btn_height = 155, 50 # 图片加载失败时的默认尺寸
else:
btn_width, btn_height = normal_pix.width(), normal_pix.height() # 与背景图尺寸一致
# 1. 创建按钮并设置基础属性
btn = QPushButton()
btn.setFixedSize(btn_width, btn_height) # 按钮尺寸匹配背景图
btn.setText(text) # 用按钮自带文字替代QLabel
# 2. 设置鼠标手势
btn.setCursor(Qt.PointingHandCursor)
# 3. 样式表控制文字16px+居中+颜色、背景图、hover效果
btn.setStyleSheet(f"""
QPushButton {{
/* 背景图设置 */
background-image: url({normal_img});
background-repeat: no-repeat;
background-position: center;
border: none;
font-size: 20px;
color: #3bfff8;
text-align: center;
}}
QPushButton:hover {{
/* hover时切换背景图 */
background-image: url({hover_img});
/* hover时文字变色 */
color: #001c83;
}}
""")
return btn
def createLongButton(self, text, normal_img, hover_img):
"""创建长按钮"""
# 以正常状态图片尺寸为准,确定按钮宽高
normal_pix = QPixmap(normal_img)
if normal_pix.isNull():
print(f"警告:{normal_img} 加载失败")
btn_width, btn_height = 339, 50 # 图片加载失败时的默认尺寸
else:
btn_width, btn_height = normal_pix.width(), normal_pix.height()
# 1. 创建按钮并设置基础属性
btn = QPushButton()
btn.setFixedSize(btn_width, btn_height) # 按钮尺寸匹配背景图
btn.setText(text)
# 2. 设置鼠标手势
btn.setCursor(Qt.PointingHandCursor)
# 3. 样式表控制文字20px字体+居中+颜色、背景图、hover效果
btn.setStyleSheet(f"""
QPushButton {{
background-image: url({normal_img});
background-repeat: no-repeat;
background-position: center;
border: none;
font-size: 20px;
color: #3bfff8;
text-align: center;
}}
QPushButton:hover {{
background-image: url({hover_img});
color: #001c83;
}}
""")
return btn
def createAutoModeWidget(self):
"""创建自动模式组件"""
bg_img = ":/icons/images/底部短按钮1.png"
bg_pix = QPixmap(bg_img)
if bg_pix.isNull():
print(f"警告:{bg_img} 加载失败")
widget_width, widget_height = 150, 40
else:
widget_width, widget_height = bg_pix.width(), bg_pix.height()
widget = QWidget()
widget.setFixedSize(widget_width, widget_height)
widget.setObjectName("autoModeWidget")
widget.setStyleSheet(f"""
QWidget #autoModeWidget{{
background-image: url({bg_img});
background-repeat: no-repeat;
background-position: center;
border: none;
}}
""")
layout = QHBoxLayout(widget)
layout.setContentsMargins(10, 0, 10, 0)
layout.setSpacing(10)
# 文字
text_label = QLabel("自动模式")
text_label.setStyleSheet("color: #3bfff8; font-size: 20px;")
layout.addWidget(text_label, alignment=Qt.AlignVCenter)
# 开关
self.auto_switch = SwitchButton()
layout.addWidget(self.auto_switch, alignment=Qt.AlignVCenter | Qt.AlignRight)
return widget
def showVersionInfo(self):
"""显示版本信息弹窗"""
info = "智能浇筑系统\n版本V1.0\n"
QMessageBox.information(self, "版本信息", info)
def paintEvent(self, event):
"""绘制背景图"""
super().paintEvent(event)
if not self.bg_pixmap.isNull():
painter = QPainter(self)
painter.drawPixmap(0, 0, self.bg_pixmap)
if __name__ == "__main__":
import sys
from PySide6.QtWidgets import QApplication
app = QApplication(sys.argv)
widget = BottomControlWidget()
widget.setStatusIcon("error", 33)
widget.show()
sys.exit(app.exec())

View File

@ -1,36 +1,62 @@
from PySide6.QtWidgets import QPushButton
from PySide6.QtGui import QPainter, QColor, QFont, Qt, QBrush
from PySide6.QtWidgets import QPushButton
from PySide6.QtGui import QPainter, QFont, Qt, QPixmap, QColor
from PySide6.QtCore import QRectF
import resources.resources_rc
# 用于 开、拱 等功能按钮
class CircularButton(QPushButton):
"""自定义圆形按钮,处理 hover 颜色变化"""
"""自定义圆形按钮"""
def __init__(self, text, parent=None):
super().__init__(text, parent)
self.setFixedSize(40, 40)
# self.normal_color = QColor(1, 18, 98) # 正常颜色 (RGB: 1,18,98)
self.normal_color = QColor(86, 119, 252)
self.hover_color = QColor(24, 253, 255) # hover 颜色 (RGB: 24,253,255)
self.current_color = self.normal_color
# 设置按钮大小为41x41与图片尺寸一致
self.setFixedSize(41, 41)
# 加载背景图片
self.normal_pixmap = QPixmap(":/icons/images/圆形按钮背景1.png") # 正常状态图片 需要修改为实际的路径
self.hover_pixmap = QPixmap(":/icons/images/圆形按钮背景2.png") # hover状态图片 需要修改为实际的路径
# 确保图片加载成功(可选:添加错误处理)
if self.normal_pixmap.isNull():
print("警告圆形按钮背景1.png 加载失败")
if self.hover_pixmap.isNull():
print("警告圆形按钮背景2.png 加载失败")
# 字体初始化状态和颜色
self.is_hover = False
self.normal_text_color = QColor(6, 224, 239)
self.hover_text_color = QColor(3, 20, 100)
# 去除默认样式
self.setStyleSheet("background: transparent; border: none;")
def enterEvent(self, event):
self.current_color = self.hover_color
self.update()
self.is_hover = True
self.update() # 触发重绘
super().enterEvent(event)
def leaveEvent(self, event):
self.current_color = self.normal_color
self.update()
self.is_hover = False
self.update() # 触发重绘
super().leaveEvent(event)
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
# 绘制圆形背景
painter.setBrush(QBrush(self.current_color))
painter.setPen(Qt.NoPen)
painter.drawEllipse(0, 0, self.width(), self.height())
# 绘制白色文字
painter.setPen(Qt.white)
painter.setFont(QFont("Arial", 10, QFont.Bold))
painter.drawText(self.rect(), Qt.AlignCenter, self.text())
painter.setRenderHint(QPainter.RenderHint.Antialiasing) # 抗锯齿
# 绘制对应状态的背景图片
current_pixmap = self.hover_pixmap if self.is_hover else self.normal_pixmap
if not current_pixmap.isNull():
# 绘制图片并适应按钮大小(保持圆形)
painter.drawPixmap(self.rect(), current_pixmap)
# 绘制文字
# painter.setPen(self.hover_text_color if self.is_hover else self.normal_text_color)
# print("self.is_hover: ", self.is_hover)
if self.is_hover: # 根据鼠标是否解除修改字体颜色
painter.setPen(self.hover_text_color)
else:
painter.setPen( self.normal_text_color)
painter.setFont(QFont("Arial", 12, QFont.Weight.Bold))
# 在按钮中心绘制文字
painter.drawText(self.rect(), Qt.AlignmentFlag.AlignCenter, self.text())

View File

@ -0,0 +1,176 @@
from PySide6.QtWidgets import QWidget, QApplication
from PySide6.QtGui import QPainter, QPen, QBrush, QColor, QTransform
from PySide6.QtCore import Qt, QPropertyAnimation, Property, QPointF
import sys
# 料斗夹爪部分,包括夹爪开合动画
class ClampWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self._angle = 0.0 # 开合角度0-70度单个夹具旋转角度为angle/2
self.setFixedSize(176, 127) # 绘图区域大小 240x150
# self.rot_center = QPointF(120, 30) # 旋转中心
# 重要,调整这里,设置夹爪绘制位置
self.rot_center = QPointF(88, 14) # 调整!!! 在主界面中位置需要调整这里
self.red_width = 37 # 红框宽度
self.red_height = 81 # 红框高度
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
# ---- 左红框及蓝色部分 ----
# 左红框原始顶点(严格对称,以旋转中心为轴)
left_red_vertices = [
QPointF(self.rot_center.x() - self.red_width, self.rot_center.y()), # 顶部左
QPointF(self.rot_center.x(), self.rot_center.y()), # 顶部右(旋转中心)
QPointF(self.rot_center.x(), self.rot_center.y() + self.red_height), # 底部右
QPointF(self.rot_center.x() - self.red_width, self.rot_center.y() + self.red_height) # 底部左
]
# 左红框旋转变换顺时针转angle/2度
painter.save()
transform = QTransform()
transform.translate(self.rot_center.x(), self.rot_center.y())
transform.rotate(self._angle / 2)
transform.translate(-self.rot_center.x(), -self.rot_center.y())
transformed_left_red = [transform.map(p) for p in left_red_vertices]
# 绘制左红框辅助线
# painter.setPen(QPen(QColor(255, 0, 0), 1, Qt.DashLine))
# painter.setBrush(Qt.NoBrush)
# painter.drawPolygon(transformed_left_red)
painter.restore()
# 绘制左蓝色部分 86, 180, 239
# painter.setPen(QPen(QColor(0, 120, 255), 2))
# painter.setBrush(QBrush(QColor(0, 170, 255, 180)))
painter.setPen(QPen(QColor(66, 152, 215, 190), 2))
painter.setBrush(QBrush(QColor(86, 180, 239)))
# 蓝色顶点顶部中间17px直线 + 红框内边界
top_mid = QPointF(
transformed_left_red[0].x() + self.red_width/2 - 10,
transformed_left_red[0].y()
)
top_end = QPointF(top_mid.x() + 17, top_mid.y())
bottom_left = transformed_left_red[3]
# ============================== 特殊计算 bottom_right1 和 bottom_right2 ======
# 定义端点
A = transformed_left_red[1] # 左红框顶部右顶点
B = transformed_left_red[2] # 左红框底部右顶点
t = 0.7 # 70%位置
# 线性插值计算bottom_left2
bottom_right2_x = A.x() + t * (B.x() - A.x())
bottom_right2_y = A.y() + t * (B.y() - A.y())
bottom_right1 = QPointF(bottom_right2_x - 5, bottom_right2_y - 8) # 基于bottom_right2 计算坐标
bottom_right2 = QPointF(bottom_right2_x, bottom_right2_y)
# ===========================================================================
bottom_right3 = QPointF(transformed_left_red[2].x(), transformed_left_red[2].y())
left_blue_vertices = [
top_mid,
top_end,
bottom_right1,
bottom_right2,
bottom_right3,
bottom_left,
top_mid
]
painter.drawPolygon(left_blue_vertices)
# ---- 右红框及蓝色部分 ----
# 右红框原始顶点(与左红框完全对称)
right_red_vertices = [
QPointF(self.rot_center.x(), self.rot_center.y()), # 顶部左(旋转中心)
QPointF(self.rot_center.x() + self.red_width, self.rot_center.y()), # 顶部右
QPointF(self.rot_center.x() + self.red_width, self.rot_center.y() + self.red_height), # 底部右
QPointF(self.rot_center.x(), self.rot_center.y() + self.red_height) # 底部左
]
# 右红框旋转变换逆时针转angle/2度
painter.save()
transform = QTransform()
transform.translate(self.rot_center.x(), self.rot_center.y())
transform.rotate(-self._angle / 2)
transform.translate(-self.rot_center.x(), -self.rot_center.y())
transformed_right_red = [transform.map(p) for p in right_red_vertices]
# 绘制右红框辅助线
# painter.setPen(QPen(QColor(255, 0, 0), 1, Qt.DashLine))
# painter.setBrush(Qt.NoBrush)
# painter.drawPolygon(transformed_right_red)
painter.restore()
# 绘制右蓝色部分 83, 175, 234 66, 152, 215
painter.setPen(QPen(QColor(66, 152, 215, 190), 2))
painter.setBrush(QBrush(QColor(86, 180, 239)))
# painter.setPen(QPen(QColor(0, 120, 255), 2))
# painter.setBrush(QBrush(QColor(0, 170, 255, 180)))
# 蓝色顶点顶部中间17px直线 + 红框内边界
top_mid = QPointF(
transformed_right_red[1].x() - self.red_width/2 + 10,
transformed_right_red[1].y()
)
top_end = QPointF(top_mid.x() - 17, top_mid.y())
# ============================== 特殊计算 bottom_left1 和 bottom_left2 =======
# 定义端点
A = transformed_right_red[0] # 右红框顶部左顶点
B = transformed_right_red[3] # 右红框底部左顶点
t = 0.7 # 70%位置
# 线性插值计算bottom_left2
bottom_left2_x = A.x() + t * (B.x() - A.x())
bottom_left2_y = A.y() + t * (B.y() - A.y())
bottom_left1 = QPointF(bottom_left2_x + 5, bottom_left2_y - 8) # 基于bottom_left2 计算坐标
bottom_left2 = QPointF(bottom_left2_x, bottom_left2_y)
# ===========================================================================
bottom_left3 = transformed_right_red[3]
bottom_right = transformed_right_red[2]
right_blue_vertices = [
top_mid,
top_end,
bottom_left1,
bottom_left2,
bottom_left3,
bottom_right,
top_mid
]
painter.drawPolygon(right_blue_vertices)
# ---- 角度属性(用于动画)----
def get_angle(self):
return self._angle
def set_angle(self, angle):
# 动画夹爪打开,限制范围为 0 到 70度
# 注意为了动画显示效果就算超过70度也只按70度计算
self._angle = max(0.0, min(70.0, angle))
self.update()
angle = Property(float, get_angle, set_angle)
# 测试动画
def testAnimation(self, target_angle, duration=6):
self.animation = QPropertyAnimation(self, b"angle")
self.animation.setDuration(duration * 1000) # duration单位为秒
self.animation.setStartValue(self._angle)
self.animation.setEndValue(target_angle)
self.animation.start()
if __name__ == "__main__":
app = QApplication(sys.argv)
widget = ClampWidget()
widget.show()
# widget.testAnimation(70)
close_anim = QPropertyAnimation(widget, b"angle")
close_anim.setDuration(6000)
close_anim.setStartValue(0)
close_anim.setEndValue(60)
close_anim.start()
# widget.set_angle(30)
sys.exit(app.exec())

View File

@ -0,0 +1,214 @@
import sys
from PySide6.QtWidgets import (
QApplication,
QWidget,
QVBoxLayout,
QHBoxLayout,
QLabel,
QPushButton,
)
from PySide6.QtGui import QPixmap
from PySide6.QtCore import Qt
import resources.resources_rc
# 传送系统控件 (包括传送带 和 料斗)
class ConveyorSystemWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("料斗与传送带界面")
self.setFixedSize(443, 190)
self.init_ui()
self._bind()
def init_ui(self):
# 主布局
main_layout = QVBoxLayout(self)
main_layout.setSpacing(0) # 组件间间距
# main_layout.setContentsMargins(20, 20, 20, 20)
main_layout.setContentsMargins(10, 120, 6, 3)
# 添加料斗组件
self.hopper_widget = self.create_upper_hopper()
# main_layout.addWidget(self.hopper_widget)
self.hopper_widget.setParent(self) # 明确父对象为窗口,确保显示在窗口上
self.hopper_widget.move(190, 7) # 中间的过渡位置
self.hopper_widget.setHidden(True)
# 添加传送带组件
self.conveyor_widget = self.create_conveyor()
main_layout.addWidget(self.conveyor_widget, alignment=Qt.AlignLeft)
# 添加传送带控制按钮
self.btn_layout = self.create_conveyor_buttons()
main_layout.addLayout(self.btn_layout)
def create_upper_hopper(self):
"""创建简化版上位料斗(移除了按钮、文字标签等元素)"""
group = QWidget()
layout = QVBoxLayout(group)
layout.setSpacing(0)
layout.setContentsMargins(0, 0, 0, 0)
# 外框图片
outer_img = ":/icons/images/料斗1.png"
outer_pixmap = QPixmap(outer_img)
if outer_pixmap.isNull():
print(f"警告:图片 {outer_img} 加载失败,请检查路径!")
return group
group.setFixedSize(outer_pixmap.size()) # 设置尺寸, 大小和外框一样
# 背景容器
upper_bg_widget = QWidget()
upper_bg_widget.setFixedSize(outer_pixmap.width(), outer_pixmap.height())
upper_bg_widget.setStyleSheet(
f"background-image: url({outer_img}); "
"background-repeat: no-repeat; "
"background-position: center;"
)
layout.addWidget(upper_bg_widget, alignment=Qt.AlignCenter)
# 内框图片
inner_img = ":/icons/images/料斗2.png"
inner_pixmap = QPixmap(inner_img)
if not inner_pixmap.isNull():
upper_inner_label = QLabel(upper_bg_widget)
upper_inner_label.setPixmap(inner_pixmap)
upper_inner_label.setFixedSize(inner_pixmap.width(), inner_pixmap.height())
upper_inner_label.move(14, 9) # 保持原位置
return group
def create_conveyor(self):
"""创建传送带组件包含左右齿轮group容器背景为传送带图片"""
group = QWidget()
group.setObjectName("conveyorGroup")
# 1. 加载传送带图片设置group的尺寸和背景
conveyor_path = ":/icons/images/传送带.png" # 需要替换为实际的路径
conveyor_pix = QPixmap(conveyor_path)
if conveyor_pix.isNull():
print("警告:传送带图片加载失败!请检查图片路径是否正确!")
else:
# group的尺寸 = 传送带图片尺寸(保证背景图完整显示)
group.setFixedSize(conveyor_pix.size())
group.setStyleSheet(f"""
#conveyorGroup {{
background-image: url({conveyor_path});
background-repeat: no-repeat;
background-position: center;
}}
""")
# 2. 给group设置布局用于放置左右齿轮
layout = QHBoxLayout(group)
layout.setSpacing(0) # 齿轮与容器边缘无间距
# 内边距(根据实际图片调整)
layout.setContentsMargins(3, 3, 2, 4)
# 3. 左侧齿轮直接放在group的布局里层级在背景之上
left_gear = QLabel(group)
left_gear_pix = QPixmap(":/icons/images/传送带齿轮.png")
if left_gear_pix.isNull():
print("警告:左侧齿轮图片加载失败!")
else:
left_gear.setPixmap(left_gear_pix)
left_gear.setFixedSize(left_gear_pix.size())
# 左对齐,让齿轮靠在传送带背景的左端
layout.addWidget(left_gear, alignment=Qt.AlignLeft | Qt.AlignVCenter)
# 4. 右侧齿轮通过addStretch推到最右边
right_gear = QLabel(group)
right_gear_pix = QPixmap(":/icons/images/传送带齿轮.png")
if right_gear_pix.isNull():
print("警告:右侧齿轮图片加载失败!")
else:
right_gear.setPixmap(right_gear_pix)
right_gear.setFixedSize(right_gear_pix.size())
# 右对齐,让齿轮靠在传送带背景的右端
layout.addWidget(right_gear, alignment=Qt.AlignRight | Qt.AlignVCenter)
return group
def create_conveyor_buttons(self):
"""创建传送带控制按钮(左右箭头按钮)"""
layout = QHBoxLayout()
layout.setSpacing(0) # 两个按钮之间的间距
# 左侧按钮
self.left_btn = QPushButton()
self.left_btn.setFixedSize(25, 25)
self.left_btn.setStyleSheet(
"""
QPushButton {
background-image: url(:/icons/images/传送带箭头按钮左1.png);
background-repeat: no-repeat;
background-position: center;
border: none;
}
QPushButton:hover {
background-image: url(:/icons/images/传送带箭头按钮左2.png);
}
QPushButton:pressed {
background-image: url(:/icons/images/传送带箭头按钮左2.png);
}
"""
)
# 右侧按钮
self.right_btn = QPushButton()
self.right_btn.setFixedSize(25, 25)
self.right_btn.setStyleSheet(
"""
QPushButton {
background-image: url(:/icons/images/传送带箭头按钮右1.png);
background-repeat: no-repeat;
background-position: center;
border: none;
}
QPushButton:hover {
background-image: url(:/icons/images/传送带箭头按钮右2.png);
}
QPushButton:pressed {
background-image: url(:/icons/images/传送带箭头按钮右2.png);
}
"""
)
# addStretch调整按钮的位置
layout.addStretch(1)
layout.addWidget(self.left_btn, alignment=Qt.AlignLeft)
layout.addStretch(3)
layout.addWidget(self.right_btn, alignment=Qt.AlignLeft)
layout.addStretch(4)
return layout
def _bind(self):
# self.left_btn.clicked.connect(self.moveHopperBelowMixer)
# self.right_btn.clicked.connect(self.moveHopperToTransition)
pass
# 移动料斗到搅拌机下方 (传送带中间位置)
def moveHopperBelowMixer(self):
self.hopper_widget.move(34, 7) # 搅拌机下方坐标
# 移动料斗到中间过渡的位置
def moveHopperToTransition(self):
"""将料斗移动到中间过渡位置(用于位置切换过程)"""
self.hopper_widget.move(190, 7) # 中间过渡坐标
# 隐藏料斗 (用于传送带中 料斗向右移动完成之后)
def hideHopper(self):
self.hopper_widget.setHidden(True)
# 显示料斗 (用于传送带中 料斗开始向左移动时)
def showHopper(self):
self.hopper_widget.setHidden(False)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = ConveyorSystemWidget()
window.show()
sys.exit(app.exec())

View File

@ -7,7 +7,9 @@ import sys
# 圆形按钮
from .circular_button import CircularButton
import resources.resource_rc
from .clamp_widget import ClampWidget
import resources.resources_rc
class HopperWidget(QWidget):
# 上料斗破拱信号
@ -28,7 +30,8 @@ class HopperWidget(QWidget):
# 料斗控制界面的固定大小为 332x482
# 需要根据具体的料斗的图片来调整
self.setFixedSize(356, 496)
# self.setFixedSize(356, 496)
self.setFixedSize(356, 516)
# 创建上位和下位料斗
self.upper_hopper = self.create_upper_hopper()
@ -49,7 +52,8 @@ class HopperWidget(QWidget):
# 上位的 料斗控件的固定尺寸 !!!!
# 注意: 这里需要根据具体的料斗图片来修改
group.setFixedSize(332, 202)
# group.setFixedSize(332, 202)
group.setFixedSize(332, 222) # 调整!!
# group.setStyleSheet("background-color: green")
layout = QVBoxLayout(group)
@ -58,13 +62,22 @@ class HopperWidget(QWidget):
# 标题标签(上位)
self.upper_title_label = QLabel("上位料斗")
self.upper_title_label.setFixedSize(79, 17)
self.upper_title_label.setFixedSize(117, 23)
self.upper_title_label.setAlignment(Qt.AlignCenter)
self.upper_title_label.setStyleSheet("color: #2fd3f2; font-size: 14px; font-weight: bold;")
# self.upper_title_label.setStyleSheet("color: #2fd3f2; font-size: 14px; font-weight: bold;")
# self.upper_title_label.setStyleSheet("color: #0bffff; font-size: 18px;")
self.upper_title_label.setStyleSheet("""
color: #0bffff;
font-size: 18px;
background-image: url(:/icons/images/文字标题底.png);
background-repeat: no-repeat;
background-position: center;
margin-bottom: 4px;
""")
layout.addWidget(self.upper_title_label, alignment=Qt.AlignCenter)
# 加载外框图片
outer_img = ":/icons/images/上位料斗1.png"
outer_img = ":/icons/images/料斗1.png"
outer_pixmap = QPixmap(outer_img)
if outer_pixmap.isNull():
print(f"警告:图片 {outer_img} 加载失败,请检查路径!")
@ -77,9 +90,10 @@ class HopperWidget(QWidget):
self.upper_bg_widget.setFixedSize(outer_width, outer_height)
self.upper_bg_widget.setStyleSheet(f"background-image: url({outer_img}); background-repeat: no-repeat; background-position: center;")
layout.addWidget(self.upper_bg_widget, alignment=Qt.AlignCenter)
# 内框图片(上位)
inner_img = ":/icons/images/上位料斗2.png"
inner_img = ":/icons/images/料斗2.png"
inner_pixmap = QPixmap(inner_img)
if not inner_pixmap.isNull():
self.upper_inner_label = QLabel(self.upper_bg_widget)
@ -91,10 +105,10 @@ class HopperWidget(QWidget):
status_img = ":/icons/images/料斗状态绿.png"
status_pixmap = QPixmap(status_img)
if not status_pixmap.isNull():
status_pixmap = status_pixmap.scaled(22, 22, Qt.KeepAspectRatio, Qt.SmoothTransformation)
status_pixmap = status_pixmap.scaled(13, 13, Qt.KeepAspectRatio, Qt.SmoothTransformation)
self.upper_status_label = QLabel(self.upper_bg_widget)
self.upper_status_label.setPixmap(status_pixmap)
self.upper_status_label.move(32, 18)
self.upper_status_label.move(22, 12)
self.upper_status_label.setScaledContents(False)
self.upper_status_label.setStyleSheet("background: none;")
@ -102,29 +116,31 @@ class HopperWidget(QWidget):
arch_img = ":/icons/images/破拱.png"
arch_pixmap = QPixmap(arch_img)
if not arch_pixmap.isNull():
arch_pixmap = arch_pixmap.scaled(24, 21, Qt.KeepAspectRatio, Qt.SmoothTransformation)
arch_pixmap = arch_pixmap.scaled(16, 13, Qt.KeepAspectRatio, Qt.SmoothTransformation)
self.upper_arch_label = QLabel(self.upper_bg_widget)
self.upper_arch_label.setPixmap(arch_pixmap)
self.upper_arch_label.move(outer_width - 56, 18)
self.upper_arch_label.move(outer_width - 39, 12)
self.upper_arch_label.setStyleSheet("background: none;")
self.upper_arch_label.setHidden(True) # 初始,不破拱状态,隐藏
# 重量文字(上位)
self.upper_weight_label = QLabel("5000kg", self.upper_bg_widget)
self.upper_weight_label.setAlignment(Qt.AlignCenter)
self.upper_weight_label.setStyleSheet("background: none; background-color: #262c38; color: #79c053; font-size: 14px; font-weight: bold;")
self.upper_weight_label.setFixedSize(93, 22)
self.upper_weight_label.move(outer_width//2 - 46, outer_height//2 - 30)
self.upper_weight_label.setStyleSheet("background: none; background-color: #003669; color: #16ffff; font-size: 18px;")
# self.upper_weight_label.setFixedSize(93, 22)
self.upper_weight_label.setFixedSize(120, 29)
self.upper_weight_label.move(outer_width//2 - 60, outer_height//2 - 46)
# 额外文字(上位)
self.upper_extra_label = QLabel("2.0方(预估)", self.upper_bg_widget)
self.upper_extra_label.setAlignment(Qt.AlignCenter)
self.upper_extra_label.setStyleSheet("background: none; background-color: #262c38; color: #79c053; font-size: 14px; font-weight: bold;")
self.upper_extra_label.setFixedSize(93, 22)
self.upper_extra_label.move(outer_width//2 - 46, outer_height//2)
# #262c38 #16ffff #131427 #003669
self.upper_extra_label.setStyleSheet("background: none; background-color: #003669; color: #16ffff; font-size: 18px;")
self.upper_extra_label.setFixedSize(120, 29)
self.upper_extra_label.move(outer_width//2 - 60, outer_height//2 - 13)
# 料斗夹具图片(上位)
# clamp_img = ":/icons/images/料斗夹具.png"
# clamp_img = "料斗夹具.png"
# clamp_pixmap = QPixmap(clamp_img)
# if not clamp_pixmap.isNull():
# self.upper_clamp_label = QLabel()
@ -134,29 +150,35 @@ class HopperWidget(QWidget):
# 料斗夹具图片上位固定152x60尺寸图片自适应缩放
# 注意:目前由于给出的 料斗夹具图片尺寸大了,只能够先缩放,后期图片对了,可以用上面的
clamp_img = ":/icons/images/料斗夹具.png"
clamp_pixmap = QPixmap(clamp_img)
if not clamp_pixmap.isNull():
self.upper_clamp_label = QLabel()
# 1. 固定QLabel尺寸为152x60
self.upper_clamp_label.setFixedSize(152, 60)
# 2. 图片缩放到152x60保持比例平滑缩放
scaled_pixmap = clamp_pixmap.scaled(
152, 60,
Qt.KeepAspectRatio, # 保持图片原始比例,避免变形
Qt.SmoothTransformation # 平滑缩放,提升显示效果
)
self.upper_clamp_label.setPixmap(scaled_pixmap)
# 3. 确保图片在QLabel中居中显示
self.upper_clamp_label.setAlignment(Qt.AlignCenter)
layout.addWidget(self.upper_clamp_label, alignment=Qt.AlignCenter)
# clamp_img = "料斗夹具.png"
# clamp_pixmap = QPixmap(clamp_img)
# if not clamp_pixmap.isNull():
# self.upper_clamp_label = QLabel()
# # 1. 固定QLabel尺寸为152x60 51,84
# self.upper_clamp_label.setFixedSize(152, 60)
# # 2. 图片缩放到152x60保持比例平滑缩放
# scaled_pixmap = clamp_pixmap.scaled(
# 152, 60,
# Qt.KeepAspectRatio, # 保持图片原始比例,避免变形
# Qt.SmoothTransformation # 平滑缩放,提升显示效果
# )
# self.upper_clamp_label.setPixmap(scaled_pixmap)
# # 3. 确保图片在QLabel中居中显示
# self.upper_clamp_label.setAlignment(Qt.AlignCenter)
# layout.addWidget(self.upper_clamp_label, alignment=Qt.AlignCenter)
# 按钮(上位)
# 夹具容器
self.upper_clamp_widget = ClampWidget()
layout.addWidget(self.upper_clamp_widget, alignment=Qt.AlignCenter)
self.upper_bg_widget.raise_() # 夹具图层位于 upper_bg_widget之下
# 按钮(上位料斗)
self.upper_open_btn = CircularButton("", group)
self.upper_open_btn.move(60, 153) # 上位料斗的开按钮位置
# self.upper_open_btn.move(60, 153) # 上位料斗的开按钮位置
self.upper_open_btn.move(290, 90)
self.upper_close_btn = CircularButton("", group)
self.upper_close_btn.move(233, 153) # 上位料斗的关按钮位置
# self.upper_close_btn = CircularButton("关", group)
# self.upper_close_btn.move(233, 153) # 上位料斗的关按钮位置
self.upper_arch_btn = CircularButton("", group)
self.upper_arch_btn.move(290, 37) # 上位料斗的破拱按钮位置
@ -167,6 +189,17 @@ class HopperWidget(QWidget):
self.upper_arch_btn.clicked.connect(self.onUpperArchBreaking)
self.lower_arch_btn.clicked.connect(self.onLowerArchBreaking)
self.upper_open_btn.clicked.connect(self.onUpperClampOpen)
self.lower_open_btn.clicked.connect(self.onLowerClampOpen)
@Slot()
def onUpperClampOpen(self):
self.upper_clamp_widget.testAnimation(target_angle=60, duration=6) # 测试6秒打开60度
@Slot()
def onLowerClampOpen(self):
self.lower_clamp_widget.testAnimation(target_angle=25, duration=6) # 测试6秒打开30度
@Slot()
def onUpperArchBreaking(self):
if self.upper_arch_breaking_status == False: # 不破拱状态
@ -199,7 +232,8 @@ class HopperWidget(QWidget):
# 下位的 料斗控件的固定尺寸 !!!!
# 注意: 这里需要根据具体的料斗图片来修改
group.setFixedSize(332, 280)
# group.setFixedSize(332, 280)
group.setFixedSize(332, 222)
# group.setStyleSheet("background-color: black")
layout = QVBoxLayout(group)
@ -208,13 +242,22 @@ class HopperWidget(QWidget):
# 标题标签(下位)
self.lower_title_label = QLabel("低位料斗")
self.lower_title_label.setFixedSize(79, 17)
self.lower_title_label.setFixedSize(117, 23)
self.lower_title_label.setAlignment(Qt.AlignCenter)
self.lower_title_label.setStyleSheet("color: #2fd3f2; font-size: 14px; font-weight: bold;")
# self.lower_title_label.setStyleSheet("color: #2fd3f2; font-size: 14px; font-weight: bold;")
# self.lower_title_label.setStyleSheet("color: #0bffff; font-size: 18px;")
self.lower_title_label.setStyleSheet("""
color: #0bffff;
font-size: 18px;
background-image: url(:/icons/images/文字标题底.png);
background-repeat: no-repeat;
background-position: center;
margin-bottom: 4px;
""")
layout.addWidget(self.lower_title_label, alignment=Qt.AlignCenter)
# 加载外框图片
outer_img = ":/icons/images/下位料斗1.png"
outer_img = ":/icons/images/料斗1.png"
outer_pixmap = QPixmap(outer_img)
if outer_pixmap.isNull():
print(f"警告:图片 {outer_img} 加载失败,请检查路径!")
@ -229,7 +272,7 @@ class HopperWidget(QWidget):
layout.addWidget(self.lower_bg_widget, alignment=Qt.AlignCenter)
# 内框图片(下位)
inner_img = ":/icons/images/下位料斗2.png"
inner_img = ":/icons/images/料斗2.png"
inner_pixmap = QPixmap(inner_img)
if not inner_pixmap.isNull():
self.lower_inner_label = QLabel(self.lower_bg_widget)
@ -237,14 +280,14 @@ class HopperWidget(QWidget):
self.lower_inner_label.setFixedSize(inner_pixmap.width(), inner_pixmap.height())
self.lower_inner_label.move(14, 9)
# 状态图片(下位,红色
# 状态图片(下位)
status_img = ":/icons/images/料斗状态绿.png"
status_pixmap = QPixmap(status_img)
if not status_pixmap.isNull():
status_pixmap = status_pixmap.scaled(22, 22, Qt.KeepAspectRatio, Qt.SmoothTransformation)
status_pixmap = status_pixmap.scaled(13, 13, Qt.KeepAspectRatio, Qt.SmoothTransformation)
self.lower_status_label = QLabel(self.lower_bg_widget)
self.lower_status_label.setPixmap(status_pixmap)
self.lower_status_label.move(32, 18)
self.lower_status_label.move(22, 12)
self.lower_status_label.setScaledContents(False)
self.lower_status_label.setStyleSheet("background: none;")
@ -252,29 +295,35 @@ class HopperWidget(QWidget):
arch_img = ":/icons/images/破拱.png"
arch_pixmap = QPixmap(arch_img)
if not arch_pixmap.isNull():
arch_pixmap = arch_pixmap.scaled(24, 21, Qt.KeepAspectRatio, Qt.SmoothTransformation)
arch_pixmap = arch_pixmap.scaled(16, 13, Qt.KeepAspectRatio, Qt.SmoothTransformation)
self.lower_arch_label = QLabel(self.lower_bg_widget)
self.lower_arch_label.setPixmap(arch_pixmap)
self.lower_arch_label.move(outer_width - 56, 18)
self.lower_arch_label.move(outer_width - 39, 12)
self.lower_arch_label.setStyleSheet("background: none;")
self.lower_arch_label.setHidden(True) # 初始,不破拱状态,隐藏
# 重量文字(下位)
self.lower_weight_label = QLabel("5000kg", self.lower_bg_widget)
self.lower_weight_label.setStyleSheet("background: none; background-color: #262c38; color: #79c053; font-size: 14px;font-weight: bold;")
# self.lower_weight_label.setStyleSheet("background: none; background-color: #262c38; color: #79c053; font-size: 14px;font-weight: bold;")
self.lower_weight_label.setStyleSheet("background: none; background-color: #003669; color: #16ffff; font-size: 18px;")
self.lower_weight_label.setAlignment(Qt.AlignCenter)
self.lower_weight_label.setFixedSize(93, 22)
self.lower_weight_label.move(outer_width//2 - 46, outer_height//2 - 46)
# self.lower_weight_label.setFixedSize(93, 22)
self.lower_weight_label.setFixedSize(120, 29)
# self.lower_weight_label.move(outer_width//2 - 46, outer_height//2 - 46)
self.lower_weight_label.move(outer_width//2 - 60, outer_height//2 - 46)
# 额外文字(下位)
self.lower_extra_label = QLabel("开: 25°", self.lower_bg_widget)
self.lower_extra_label.setAlignment(Qt.AlignCenter)
self.lower_extra_label.setStyleSheet("background: none; background-color: #262c38; color: #79c053; font-size: 14px;font-weight: bold;")
self.lower_extra_label.setFixedSize(93, 22)
self.lower_extra_label.move(outer_width//2 - 46, outer_height//2 - 20)
# self.lower_extra_label.setStyleSheet("background: none; background-color: #262c38; color: #79c053; font-size: 14px;font-weight: bold;")
self.lower_extra_label.setStyleSheet("background: none; background-color: #003669; color: #16ffff; font-size: 18px;")
# self.lower_extra_label.setFixedSize(93, 22)
self.lower_extra_label.setFixedSize(120, 29)
# self.lower_extra_label.move(outer_width//2 - 46, outer_height//2 - 20)
self.lower_extra_label.move(outer_width//2 - 60, outer_height//2 - 13)
# 料斗夹具图片(下位)
# clamp_img = ":/icons/images/料斗夹具.png"
# clamp_img = "料斗夹具.png"
# clamp_pixmap = QPixmap(clamp_img)
# if not clamp_pixmap.isNull():
# self.lower_clamp_label = QLabel()
@ -283,29 +332,35 @@ class HopperWidget(QWidget):
# layout.addWidget(self.lower_clamp_label, alignment=Qt.AlignCenter)
# 目前由于给出的 料斗夹具图片尺寸大了,只能够先缩放,后期图片对了,可以用上面的
clamp_img = ":/icons/images/料斗夹具.png"
clamp_pixmap = QPixmap(clamp_img)
if not clamp_pixmap.isNull():
self.lower_clamp_label = QLabel()
# 1. 固定QLabel尺寸为152x60
self.lower_clamp_label.setFixedSize(152, 60)
# 2. 图片缩放到152x60保持比例平滑缩放
scaled_pixmap = clamp_pixmap.scaled(
152, 60,
Qt.KeepAspectRatio, # 保持图片原始比例,避免变形
Qt.SmoothTransformation # 平滑缩放,提升显示效果
)
self.lower_clamp_label.setPixmap(scaled_pixmap)
# 3. 确保图片在QLabel中居中显示
self.lower_clamp_label.setAlignment(Qt.AlignCenter)
layout.addWidget(self.lower_clamp_label, alignment=Qt.AlignCenter)
# clamp_img = "料斗夹具.png"
# clamp_pixmap = QPixmap(clamp_img)
# if not clamp_pixmap.isNull():
# self.lower_clamp_label = QLabel()
# # 1. 固定QLabel尺寸为152x60
# self.lower_clamp_label.setFixedSize(152, 60)
# # 2. 图片缩放到152x60保持比例平滑缩放
# scaled_pixmap = clamp_pixmap.scaled(
# 152, 60,
# Qt.KeepAspectRatio, # 保持图片原始比例,避免变形
# Qt.SmoothTransformation # 平滑缩放,提升显示效果
# )
# self.lower_clamp_label.setPixmap(scaled_pixmap)
# # 3. 确保图片在QLabel中居中显示
# self.lower_clamp_label.setAlignment(Qt.AlignCenter)
# layout.addWidget(self.lower_clamp_label, alignment=Qt.AlignCenter)
# 夹具容器
self.lower_clamp_widget = ClampWidget()
layout.addWidget( self.lower_clamp_widget, alignment=Qt.AlignCenter)
self.lower_bg_widget.raise_() # 夹具图层位于 self.lower_bg_widget之下
# 按钮(下位)
self.lower_open_btn = CircularButton("", group)
self.lower_open_btn.move(60, 233) # 下位料斗的开按钮位置
# self.lower_open_btn.move(60, 233) # 下位料斗的开按钮位置
self.lower_open_btn.move(290, 90)
self.lower_close_btn = CircularButton("", group)
self.lower_close_btn.move(233, 233) # 下位料斗的关按钮位置
# self.lower_close_btn = CircularButton("关", group)
# self.lower_close_btn.move(233, 233) # 下位料斗的关按钮位置
self.lower_arch_btn = CircularButton("", group)
self.lower_arch_btn.move(290, 34) # 下位料斗的破拱按钮位置
@ -395,6 +450,23 @@ class HopperWidget(QWidget):
)
self.lower_status_label.setPixmap(status_pixmap)
# 隐藏上料斗 (用于上料斗移动)
def hideUpperHopper(self):
self.upper_title_label.hide()
self.upper_bg_widget.hide()
self.upper_clamp_widget.hide()
self.upper_open_btn.hide()
self.upper_arch_btn.hide()
# 显示上料斗 (用于上料斗移动完成后恢复显示)
def showUpperHopper(self):
self.upper_title_label.setHidden(False)
self.upper_bg_widget.setHidden(False)
self.upper_clamp_widget.setHidden(False)
self.upper_open_btn.setHidden(False)
self.upper_arch_btn.setHidden(False)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = HopperWidget()

View File

@ -0,0 +1,61 @@
from PySide6.QtWidgets import QWidget, QLabel, QHBoxLayout
from PySide6.QtGui import QPixmap, QFont
from PySide6.QtCore import Qt
import resources.resources_rc
class MixerWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
# 初始化布局
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
self.setFixedSize(225, 102)
# 1. 创建“搅拌机”文字标签
self.text_label = QLabel("搅拌机")
self.text_label.setFixedSize(100, 23)
self.text_label.setFont(QFont("Arial", 14)) # 设置字体大小为16px
self.text_label.setAlignment(Qt.AlignCenter)
self.text_label.setStyleSheet("""
background-image: url(:/icons/images/文字标题底.png);
background-repeat: no-repeat;
background-position: center;
color: #0bffff; /* 可根据需求调整文字颜色 */
""")
layout.addWidget(self.text_label, alignment=Qt.AlignLeft)
# 2. 创建搅拌机设备及搅拌桨图标
self.device_label = QLabel()
device_pixmap = QPixmap(":/icons/images/搅拌机.png")
self.device_label.setPixmap(device_pixmap)
layout.addWidget(self.device_label, alignment=Qt.AlignLeft)
# 3. 叠加两个搅拌桨图标
self.blade1 = QLabel(self.device_label) # 从左往右第一个搅拌桨
blade1_pixmap = QPixmap(":/icons/images/搅拌桨.png")
self.blade1.setPixmap(blade1_pixmap)
self.blade1.move(
(device_pixmap.width() - blade1_pixmap.width()) // 2 - 26,
(device_pixmap.height() - blade1_pixmap.height()) // 2 - 4
)
self.blade2 = QLabel(self.device_label)
blade2_pixmap = QPixmap(":/icons/images/搅拌桨.png") # 从左往右第二个搅拌桨
self.blade2.setPixmap(blade2_pixmap)
self.blade2.move(
(device_pixmap.width() - blade2_pixmap.width()) // 2 + 31,
(device_pixmap.height() - blade2_pixmap.height()) // 2 - 4
)
# 测试代码
if __name__ == "__main__":
import sys
from PySide6.QtWidgets import QApplication, QMainWindow
app = QApplication(sys.argv)
mixer_widget = MixerWidget()
mixer_widget.show()
sys.exit(app.exec())

168
view/widgets/plan_widget.py Normal file
View File

@ -0,0 +1,168 @@
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QApplication)
from PySide6.QtCore import Qt
from PySide6.QtGui import QPainter, QPixmap
from .value_adjuster import ValueAdjuster
from .switch_button import SwitchButton
import sys
import resources.resources_rc
class PlanWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
# 加载背景图
self.bg_pixmap = QPixmap(":/icons/images/任务计划背景.png")
self.setFixedSize(self.bg_pixmap.size())
# 存储可修改控件的字典
self.controls = {
"plan_no": None, # 计划单号
"volume": None, # 计划方量ValueAdjuster
"ratio": None, # 计划配比
"status": None, # 下发状态
"auto_dispatch": None # 自动派单SwitchButton
}
# 主垂直布局
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(13, 5, 6, 15)
main_layout.setSpacing(0)
# 第一行:计划单号
row1_layout = QHBoxLayout()
row1_layout.setSpacing(0)
status_icon1 = QLabel()
status_icon1.setPixmap(QPixmap(":/icons/images/任务矩形4.png"))
status_icon1.setFixedSize(6, 6)
row1_layout.addWidget(status_icon1)
row1_layout.addSpacing(4)
label1 = QLabel("计划单号")
label1.setStyleSheet("font-size: 18px; color: #03f5ff;")
label1.setFixedSize(73, 20)
row1_layout.addWidget(label1, alignment=Qt.AlignLeft)
value1 = QLabel("PD0001")
value1.setStyleSheet("font-size: 18px; color: white;")
row1_layout.addWidget(value1)
main_layout.addLayout(row1_layout)
self.controls["plan_no"] = value1
# 第二行:计划方量
row2_layout = QHBoxLayout()
status_icon2 = QLabel()
status_icon2.setPixmap(QPixmap(":/icons/images/任务矩形1.png"))
status_icon2.setFixedSize(6, 6)
row2_layout.addWidget(status_icon2)
row2_layout.addSpacing(4)
label2 = QLabel("计划方量")
label2.setStyleSheet("font-size: 18px; color: #03f5ff;")
row2_layout.addWidget(label2)
self.fangliang_adjuster = ValueAdjuster() # 导入的方量调整控件
row2_layout.addWidget(self.fangliang_adjuster)
main_layout.addLayout(row2_layout)
self.controls["volume"] = self.fangliang_adjuster
# 第三行:计划配比
row3_layout = QHBoxLayout()
status_icon3 = QLabel()
status_icon3.setFixedSize(6, 6)
status_icon3.setPixmap(QPixmap(":/icons/images/任务矩形2.png"))
row3_layout.addWidget(status_icon3)
row3_layout.addSpacing(4)
label3 = QLabel("计划配比")
label3.setStyleSheet("font-size: 18px; color: #03f5ff;")
row3_layout.addWidget(label3)
value3 = QLabel("C55P12")
value3.setStyleSheet("font-size: 18px; color: white;")
row3_layout.addWidget(value3)
main_layout.addLayout(row3_layout)
# 存入字典
self.controls["ratio"] = value3
# 第四行:下发状态
row4_layout = QHBoxLayout()
status_icon4 = QLabel()
status_icon4.setFixedSize(6, 6)
status_icon4.setPixmap(QPixmap(":/icons/images/任务矩形3.png"))
row4_layout.addWidget(status_icon4)
row4_layout.addSpacing(4)
label4 = QLabel("下发状态")
label4.setStyleSheet("font-size: 18px; color: #03f5ff;")
row4_layout.addWidget(label4)
value4 = QLabel("计划中")
value4.setStyleSheet("font-size: 18px; color: white;")
row4_layout.addWidget(value4)
main_layout.addLayout(row4_layout)
self.controls["status"] = value4
# 第五行:自动派单
row5_layout = QHBoxLayout()
status_icon5 = QLabel()
status_icon5.setPixmap(QPixmap(":/icons/images/任务矩形5.png"))
status_icon5.setFixedSize(6, 6)
row5_layout.addWidget(status_icon5)
row5_layout.addSpacing(4)
label5 = QLabel("自动派单")
label5.setStyleSheet("font-size: 18px; color: #03f5ff;")
row5_layout.addWidget(label5)
self.switch = SwitchButton()
self.switch.setChecked(True)
row5_layout.addWidget(self.switch, alignment=Qt.AlignLeft)
main_layout.addLayout(row5_layout)
self.controls["auto_dispatch"] = self.switch
def paintEvent(self, event):
"""绘制背景图片"""
painter = QPainter(self)
painter.drawPixmap(self.rect(), self.bg_pixmap)
super().paintEvent(event)
# ------------------- 对外修改接口 -------------------
def set_plan_no(self, new_no:str):
"""修改计划单号"""
if self.controls["plan_no"]:
self.controls["plan_no"].setText(new_no)
def set_plan_volume(self, new_volume:float):
"""修改计划方量"""
if self.controls["volume"]:
self.controls["volume"].set_value(new_volume)
def set_plan_ratio(self, new_ratio:str):
"""修改计划配比"""
if self.controls["ratio"]:
self.controls["ratio"].setText(new_ratio)
def set_status(self, new_status:str):
"""修改下发状态"""
if self.controls["status"]:
self.controls["status"].setText(new_status)
def set_auto_dispatch(self, is_checked:bool):
"""修改自动派单开关状态, true: 开启自动派单, False: 关闭自动派单"""
if self.controls["auto_dispatch"]:
self.controls["auto_dispatch"].setChecked(is_checked)
if __name__ == "__main__":
app = QApplication(sys.argv)
widget = PlanWidget()
widget.show()
# 测试接口
widget.set_plan_no("PD0002") # 计划单号
widget.set_plan_ratio("C60P15") # 计划配比
widget.set_plan_volume(5) # 计划方量
widget.set_status("已下发") # 下发状态
widget.set_auto_dispatch(False) # 自动派单
sys.exit(app.exec())

View File

@ -1,46 +1,47 @@
from PySide6.QtWidgets import QApplication, QWidget, QLabel, QHBoxLayout
from PySide6.QtGui import QPixmap,QRegion,QPainter, QColor, QPainterPath
from PySide6.QtGui import QPixmap, QRegion, QPainter, QColor, QPainterPath
from PySide6.QtCore import Qt, QPropertyAnimation, Property, QSize, QRectF
import sys
class MaskedLabel(QLabel):
def __init__(self, parent=None):
super().__init__(parent)
self.setFixedSize(28, 20) # 遮罩大小
self.setFixedSize(28, 20) # 遮罩大小
self.setStyleSheet("background-color: transparent; border: none;")
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
parent_widget = self.parent()
bg_color = QColor("#f3f3f3") # 默认颜色
if parent_widget:
# 从父控件的调色板中提取背景色
bg_color = parent_widget.palette().color(parent_widget.backgroundRole())
# parent_widget = self.parent()
bg_color = QColor("#023eab") # 默认颜色 注意:需要根据背景颜色修改
# if parent_widget:
# # 从父控件的调色板中提取背景色
# bg_color = parent_widget.palette().color(parent_widget.backgroundRole())
# 用父控件背景色绘制遮罩
painter.setBrush(bg_color)
painter.setPen(Qt.NoPen)
# 步骤3绘制遮罩路径逻辑不变
path = QPainterPath()
mask_rect = QRectF(0, 0, 28, 20)
circle_radius = 10
circle_center_x = 28
circle_center_y = 10
path.addRect(mask_rect)
path.addEllipse(
circle_center_x - circle_radius,
circle_center_y - circle_radius,
circle_radius * 2,
circle_radius * 2
circle_radius * 2,
)
path.setFillRule(Qt.OddEvenFill)
painter.drawPath(path)
class LinearProductionProgress(QWidget):
def __init__(self, parent=None):
@ -48,7 +49,7 @@ class LinearProductionProgress(QWidget):
self._progress = 0 # 进度0-100
self.setFixedSize(450 + 18, 20)
self.setStyleSheet(
"""
"""
border-radius: 9px;
"""
)
@ -56,18 +57,31 @@ class LinearProductionProgress(QWidget):
# 底层背景
self.bg_label = QLabel(self)
self.bg_label.setFixedSize(450, 20)
# self.bg_label.setStyleSheet(
# """
# background-color: #e7e7e7;
# """
# )
# #011454
self.bg_label.setStyleSheet(
"""
background-color: #e7e7e7;
"""
background-color: #011454;
"""
)
self.bg_label.move(18, 0)
self.bg_label.move(17, 0) # 这里需要调整对齐
# 上层进度填充
self.fg_label = QLabel(self)
# self.fg_label.setStyleSheet(
# """
# background-color: #0052d9;
# min-width: 18px;
# """
# )
# #02f2fe
self.fg_label.setStyleSheet(
"""
background-color: #0052d9;
background-color: #02f2fe;
min-width: 18px;
"""
)
@ -80,15 +94,24 @@ class LinearProductionProgress(QWidget):
self.percent_label.setText("0%")
self.percent_label.setAlignment(Qt.AlignCenter)
self.percent_label.setFixedSize(33, 19)
# self.percent_label.setStyleSheet(
# """
# color: white;
# font-size: 12px;
# font-weight: bold;
# background-color: transparent;
# """
# )
# #02366c #001c83
self.percent_label.setStyleSheet(
"""
color: white;
font-size: 12px;
color: #001c83;
font-size: 13px;
font-weight: bold;
background-color: transparent;
"""
)
self.percent_label.move(18, 0) # 百分比标签初始位置
self.percent_label.move(18, 0) # 百分比标签初始位置
self.percent_label.raise_()
# 遮盖左侧区域
@ -111,17 +134,17 @@ class LinearProductionProgress(QWidget):
# 注意为了实现前4%的动态效果,需要从 fd_width的4%起开始计算
fg_width = int(450 * (self._progress + 4) / 100)
self.fg_label.setFixedWidth(fg_width)
# 计算百分比标签位置:进度条右边缘 - 9px偏移 - 标签宽度33px
if fg_width > 60: # 当上层进度条宽度大于60px开始移动
label_x = fg_width - 9 - 33
if fg_width > 60: # 当上层进度条宽度大于60px开始移动
label_x = fg_width - 9 - 33
# 移动百分比标签
self.percent_label.move(label_x, 0)
else:
# 复原百分比标签
# 移动回初始位置
self.percent_label.move(18, 0)
# 设置百分比标签
self.percent_label.setText(f"{self._progress}%")
@ -129,21 +152,22 @@ class LinearProductionProgress(QWidget):
class ProductionProgressWidget(QWidget):
def __init__(self):
super().__init__()
self.setFixedSize(620, 49) # 进度条控件大小
self.setFixedSize(620, 49) # 进度条控件大小
# 左侧文字标签
self.text_label = QLabel("生产进度")
self.text_label = QLabel("进度")
self.text_label.setFixedSize(112, 29)
self.text_label.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
self.text_label.setStyleSheet("font-family: 'Microsoft YaHei';font-size: 18px;")
self.text_label.setAlignment(Qt.AlignTop | Qt.AlignRight)
self.text_label.setStyleSheet("font-family: 'Microsoft YaHei';font-size: 20px;color: #16FFFF;")
# 右侧进度条
self.linear_progress = LinearProductionProgress()
self.main_layout = QHBoxLayout(self)
self.main_layout.addWidget(self.text_label)
self.main_layout.addWidget(self.linear_progress)
self.main_layout.addWidget(self.linear_progress, alignment=Qt.AlignLeft)
self.main_layout.setContentsMargins(0, 0, 0, 0)
self.main_layout.setSpacing(0)
def testProgress(self, seconds: float):
self.animation = QPropertyAnimation(self.linear_progress, b"progress")
@ -164,6 +188,6 @@ class ProductionProgressWidget(QWidget):
if __name__ == "__main__":
app = QApplication(sys.argv)
window = ProductionProgressWidget()
# window.testProgress(60) # 进度条测试
window.testProgress(60) # 进度条测试
window.show()
sys.exit(app.exec())

View File

@ -4,7 +4,7 @@ from PySide6.QtCore import Qt
import sys
import datetime
import resources.resource_rc
import resources.resources_rc
class DeviceStatusPopup(QDialog):
def __init__(self, parent=None):

View File

@ -0,0 +1,126 @@
from PySide6.QtWidgets import (
QAbstractButton,
QApplication,
QSizePolicy,
QWidget,
QVBoxLayout,
)
from PySide6.QtCore import Qt, QRect, Signal
from PySide6.QtGui import QPainter, QBrush
class SwitchButton(QAbstractButton):
# 开关切换信号
# 开: True, 关: False
switched = Signal(bool)
def __init__(self, parent=None):
super().__init__(parent)
self._checked = False
self.setCursor(Qt.CursorShape.PointingHandCursor)
# 颜色映射
self.color_keymap = {
"slider": "#16ffff",
"text": Qt.GlobalColor.white,
"on_bg": "#008ee8", # 开的时候的背景颜色
"off_bg": "#001c83", # 关的时候的背景颜色
}
self.clicked.connect(self.onClicked)
self._init_style() # 新尺寸的样式表
self._init_size_policy()
def _init_style(self):
self.setStyleSheet(
"""
SwitchButton {
font-family: "Microsoft YaHei";
font-size: 11px;
color: white;
border-radius: 9px;
margin: 2px;
min-width: 39px;
max-width: 39px;
min-height: 18px;
max-height: 18px;
}
"""
)
def _init_size_policy(self):
self.setSizePolicy(QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed)
def paintEvent(self, event):
# 1. 调整滑块间距
slider_space = 1 # 滑块与背景的间距上下左右各1px
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
try:
# 2. 绘制背景
background_rect = self.rect()
painter.setPen(Qt.PenStyle.NoPen)
bg_color = (
self.color_keymap["on_bg"]
if self._checked
else self.color_keymap["off_bg"]
)
painter.setBrush(QBrush(bg_color))
painter.drawRoundedRect(
background_rect, self.height() / 2, self.height() / 2
)
# 3. 计算新滑块尺寸
slider_width = self.height() - slider_space * 2
# 4. 计算新滑块位置
if self._checked:
slider_x = self.width() - slider_width - slider_space
else:
slider_x = slider_space
slider_y = slider_space
# 5. 绘制滑块
slider_rect = QRect(slider_x, slider_y, slider_width, slider_width)
painter.setBrush(QBrush(self.color_keymap["slider"]))
painter.drawEllipse(slider_rect)
finally:
painter.end()
def onClicked(self):
self._checked = not self._checked
self.update()
self.switched.emit(self._checked)
def setChecked(self, checked: bool):
self._checked = checked
self.update()
self.switched.emit(self._checked)
def isChecked(self) -> bool:
return self._checked
# 测试示例
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
test_window = QWidget()
test_window.setWindowTitle("开关测试")
test_window.resize(200, 100)
layout = QVBoxLayout(test_window)
layout.setContentsMargins(50, 30, 50, 30)
switch = SwitchButton()
switch.switched.connect(
lambda state: print(f"开关状态:{'选中' if state else '未选中'}")
)
layout.addWidget(switch)
test_window.show()
sys.exit(app.exec())

View File

@ -3,7 +3,7 @@ from PySide6.QtGui import QIcon
from PySide6.QtCore import QSize, Qt
import sys
import resources.resource_rc
import resources.resources_rc
class SystemButtonWidget(QWidget):
def __init__(self):

View File

@ -0,0 +1,151 @@
from PySide6.QtWidgets import (QApplication, QVBoxLayout, QPushButton, QDialog)
from PySide6.QtGui import QPixmap, QFont, QIcon, QPainter
from PySide6.QtCore import Qt, Signal
from PySide6.QtCore import Qt, Signal, QPropertyAnimation, QEasingCurve, QRect
import sys
import resources.resources_rc
class CustomButton(QPushButton):
def __init__(self, text, normal_icon, active_icon, parent=None):
super().__init__(text, parent)
self.normal_icon = normal_icon
self.active_icon = active_icon
self.setFixedSize(147, 36)
self.setCursor(Qt.PointingHandCursor)
self.update_icon(self.normal_icon)
self.setFont(QFont("", 17))
self.setStyleSheet("""
QPushButton {
color: #16ffff;
background: none;
outline: none;
border: none;
text-align: center;
background-color: transparent;
font-weight: Bold;
}
QPushButton:hover {
color: #001c83;
background-color: #16ffff;
background-repeat: no-repeat;
outline: none;
border: none;
}
""")
def update_icon(self, icon_path):
pixmap = QPixmap(icon_path)
if not pixmap.isNull():
scaled_pixmap = pixmap.scaled(24, 24, Qt.KeepAspectRatio, Qt.SmoothTransformation)
self.setIcon(QIcon(scaled_pixmap))
self.setIconSize(scaled_pixmap.size())
else:
self.setIcon(QIcon())
def enterEvent(self, event):
self.update_icon(self.active_icon)
super().enterEvent(event)
def leaveEvent(self, event):
self.update_icon(self.normal_icon)
super().leaveEvent(event)
class SystemCenterDialog(QDialog):
# 定义三个信号,对应三个按钮的点击事件
sys_setting_clicked = Signal() # 系统设置点击信号
data_center_clicked = Signal() # 数据中心点击信号
user_center_clicked = Signal() # 用户中心点击信号
def __init__(self, parent=None):
super().__init__(parent)
self.init_ui()
self.init_animations() # 初始化动画
def init_ui(self):
# 弹窗基础设置
self.setWindowTitle("系统中心")
self.setWindowFlags(Qt.FramelessWindowHint) # 隐藏默认边框
self.setWindowOpacity(0.0) # 初始状态为完全透明,实现动画效果
# 加载背景图
self.background = QPixmap(":/icons/images/系统中心弹窗背景.png")
if self.background.isNull():
print("警告:系统中心弹窗背景.png 加载失败!")
self.setFixedSize(220, 200)
else:
self.setFixedSize(self.background.size())
# 主布局
main_layout = QVBoxLayout(self)
main_layout.setContentsMargins(0, 0, 0, 6)
main_layout.setSpacing(0)
main_layout.setAlignment(Qt.AlignCenter)
# 创建按钮并绑定信号发射
self.sys_btn = CustomButton("系统设置", ":/icons/images/系统设置背景1.png", ":/icons/images/系统设置背景2.png")
self.data_btn = CustomButton("数据中心", ":/icons/images/系统数据中心背景1.png", ":/icons/images/系统数据中心背景2.png")
self.user_btn = CustomButton("用户中心", ":/icons/images/系统用户中心背景1.png", ":/icons/images/系统用户中心背景2.png")
# 按钮点击 → 发射对应信号dialog不处理业务逻辑只传递事件
self.sys_btn.clicked.connect(self.sys_setting_clicked.emit)
self.data_btn.clicked.connect(self.data_center_clicked.emit)
self.user_btn.clicked.connect(self.user_center_clicked.emit)
main_layout.addWidget(self.sys_btn)
main_layout.addWidget(self.data_btn)
main_layout.addWidget(self.user_btn)
def paintEvent(self, event):
painter = QPainter(self)
painter.drawPixmap(self.rect(), self.background)
super().paintEvent(event)
def init_animations(self):
"""初始化显示动画(可根据喜好选择或组合)"""
# 1. 淡入动画透明度从0→1
self.opacity_anim = QPropertyAnimation(self, b"windowOpacity")
self.opacity_anim.setDuration(300) # 动画时长300ms
self.opacity_anim.setStartValue(0.0)
self.opacity_anim.setEndValue(1.0)
self.opacity_anim.setEasingCurve(QEasingCurve.InOutCubic) # 缓动曲线(平滑加速减速)
# 2. 缩放动画从80%→100%大小)
self.scale_anim = QPropertyAnimation(self, b"geometry")
self.scale_anim.setDuration(300)
# 起点和终点在显示时动态设置(依赖当前弹窗位置)
self.scale_anim.setEasingCurve(QEasingCurve.OutBack) # 带弹性的缓动曲线(弹出感)
# 3. 组合动画(同时执行淡入+缩放)
from PySide6.QtCore import QParallelAnimationGroup
self.anim_group = QParallelAnimationGroup(self)
self.anim_group.addAnimation(self.opacity_anim)
self.anim_group.addAnimation(self.scale_anim)
def showEvent(self, event):
"""重写显示事件,每次显示时启动动画"""
# 必须先调用父类showEvent否则弹窗无法正常显示
super().showEvent(event)
# 动态设置缩放动画的起点(基于当前弹窗位置和大小)
current_geometry = self.geometry() # 弹窗当前位置和大小已通过move设置
# 起点缩小到80%,并保持中心位置不变
start_rect = QRect(
current_geometry.center().x() - current_geometry.width() * 0.4,
current_geometry.center().y() - current_geometry.height() * 0.4,
int(current_geometry.width() * 0.8),
int(current_geometry.height() * 0.8)
)
self.scale_anim.setStartValue(start_rect)
self.scale_anim.setEndValue(current_geometry) # 终点:原始大小
# 启动组合动画
self.anim_group.start()
if __name__ == "__main__":
app = QApplication(sys.argv)
dialog = SystemCenterDialog()
# 测试信号(实际业务在控制器中绑定)
dialog.sys_setting_clicked.connect(lambda: print("系统设置被点击"))
dialog.show()
sys.exit(app.exec())

View File

@ -0,0 +1,117 @@
from PySide6.QtWidgets import QWidget, QLabel, QHBoxLayout, QSpacerItem, QSizePolicy
from PySide6.QtGui import QPixmap, QFont, QPainter
from PySide6.QtCore import Qt, QTimer, QDateTime
import resources.resources_rc
# 自定义消息容器, 显示系统消息
class MsgContainer(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setFixedSize(770, 24)
# 加载消息区域背景图
self.bg_pixmap = QPixmap(":/icons/images/系统消息背景.png") # 替换为实际路径
if self.bg_pixmap.isNull():
print("警告:系统消息背景.png 加载失败")
# 消息区域内部布局(喇叭+文本)
msg_layout = QHBoxLayout(self)
msg_layout.setContentsMargins(0, 0, 0, 0) # 调整内边距,避免内容贴边
msg_layout.setSpacing(3) # 喇叭和文本的间距
# 消息喇叭图标
self.msg_icon = QLabel()
self.msg_icon.setFixedSize(13, 18)
# self.msg_icon.setStyleSheet("background-color:red;")
self.msg_icon.setPixmap(QPixmap(":/icons/images/系统消息喇叭.png")) # 替换为实际路径
msg_layout.addWidget(self.msg_icon, alignment=Qt.AlignVCenter | Qt.AlignLeft)
# 消息文本
current_time = QDateTime.currentDateTime().toString("hh:mm:ss")
self.msg_text = QLabel(f"{current_time} 开始启动智能浇筑系统")
self.msg_text.setFixedWidth(740)
# self.msg_text.setStyleSheet("color: white; font-size: 14px;background-color:red;") # 文本样式
self.msg_text.setStyleSheet("color: white; font-size: 16px;font-weight:Bold;")
self.msg_text.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
msg_layout.addWidget(self.msg_text)
def paintEvent(self, event):
# 绘制消息区域背景图
super().paintEvent(event) # 确保子控件正常绘制
if self.bg_pixmap.isNull():
return # 图片加载失败则不绘制
painter = QPainter(self)
painter.setRenderHint(QPainter.SmoothPixmapTransform) # 缩放平滑
painter.drawPixmap(self.rect(), self.bg_pixmap)
class SystemNavBar(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
# 设置尺寸
self.setFixedSize(1280, 80)
# 1. 加载背景图
self.bg_pixmap = QPixmap(":/icons/images/系统主界面导航栏.png") # 替换为实际图片路径
if self.bg_pixmap.isNull():
print("警告:背景图加载失败,请检查路径!")
main_layout = QHBoxLayout(self)
main_layout.setContentsMargins(9, 9, 9, 19)
main_layout.setSpacing(100) # 注意左侧的logo+系统标题的容器 和 系统消息的间隔
# 左侧区域logo + 系统标题
left_container = QWidget()
left_container.setFixedSize(400, 53)
left_layout = QHBoxLayout(left_container) # 容器内部的水平布局
left_layout.setContentsMargins(0, 0, 0, 0) # 容器内边距
left_layout.setSpacing(6) # 设置logo和标题之间的间隙为6px
# 系统logo
self.logo = QLabel()
self.logo.setFixedSize(53, 53)
self.logo.setPixmap(QPixmap(":/icons/images/系统logo.png"))
left_layout.addWidget(self.logo, alignment=Qt.AlignTop)
# 系统总标题
self.title = QLabel()
self.title.setPixmap(QPixmap(":/icons/images/系统总标题.png"))
left_layout.addWidget(self.title, alignment=Qt.AlignCenter)
main_layout.addWidget(left_container, alignment=Qt.AlignTop)
# 中间区域:系统消息(喇叭+文本+背景)
self.msg_container = MsgContainer()
main_layout.addWidget(self.msg_container, alignment=Qt.AlignBottom | Qt.AlignRight)
# 右侧区域:实时时间
self.time_label = QLabel()
self.time_label.setStyleSheet("color: white; font-size: 16px;font-weight:Bold;")
main_layout.addWidget(self.time_label, alignment= Qt.AlignTop | Qt.AlignRight)
# 启动时间更新定时器
self.timer = QTimer(self)
self.timer.timeout.connect(self.update_time)
self.timer.start(1000) # 每秒更新一次
def paintEvent(self, event):
# 2. 调用父类paintEvent
super().paintEvent(event)
# 3. 创建画家对象,绘制背景图
painter = QPainter(self)
painter.setRenderHint(QPainter.SmoothPixmapTransform) # 缩放时平滑
painter.drawPixmap(0, 0, self.bg_pixmap)
def update_time(self):
current_time = QDateTime.currentDateTime().toString("yyyy/MM/dd hh:mm:ss")
self.time_label.setText(current_time)
# 测试代码
if __name__ == "__main__":
import sys
from PySide6.QtWidgets import QApplication, QMainWindow
app = QApplication(sys.argv)
nav_bar = SystemNavBar()
nav_bar.show()
sys.exit(app.exec())

188
view/widgets/task_widget.py Normal file
View File

@ -0,0 +1,188 @@
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QMessageBox, QApplication)
from PySide6.QtCore import Qt
from PySide6.QtGui import QPainter, QPixmap, QFont
import sys
import resources.resources_rc
# 任务控件,如:管片任务、派单任务
class TaskWidget(QWidget):
def __init__(self, taskTitle:str, parent=None):
super().__init__(parent)
# 设置Widget大小与背景图一致
self.bg_pixmap = QPixmap(":/icons/images/任务信息背景1.png")
self.setFixedSize(self.bg_pixmap.size())
# 主布局(垂直)
self.main_layout = QVBoxLayout(self)
self.main_layout.setContentsMargins(0, 0, 0, 6)
self.main_layout.setSpacing(0)
# 任务标题
title_label = QLabel(taskTitle, self)
title_label.setStyleSheet("font-size: 24px; color: #16ffff;padding-top:4px;")
title_label.setAlignment(Qt.AlignCenter)
self.main_layout.addWidget(title_label, alignment=Qt.AlignTop)
# 标题字体设置
# title_font = title_label.font()
title_font = QFont("Microsoft YaHei")
title_font.setLetterSpacing(QFont.AbsoluteSpacing, 3) # 字间距3px
title_font.setWeight(QFont.DemiBold)
title_label.setFont(title_font)
# 用字典存储每个任务的可修改控件(键:任务名,值:控件字典)
self.task_controls = {} # 结构:{"task1": {"volume_label": xxx, "time_label": xxx, ...}, ...}
# 三条任务条目
self._add_task("task1", "SHRB1-3", ":/icons/images/任务矩形1.png")
self._add_task("task2", "SHRB2-3", ":/icons/images/任务矩形2.png")
self._add_task("task3", "SHRB1-3", ":/icons/images/任务矩形3.png")
def paintEvent(self, event):
"""绘制背景图片"""
painter = QPainter(self)
painter.drawPixmap(self.rect(), self.bg_pixmap)
super().paintEvent(event)
def _add_task(self, task_name, task_id, status_icon):
"添加相应的任务条目到布局,同时将其相应的控件存入字典"
# 1、创建任务条目以及相应的控件字典
item_widget, controls = self._create_task_item(task_name, task_id, status_icon)
# 2、将控件字典存入 相应的 任务条目字典
self.task_controls[task_name] = controls
# 3、将任务条目添加到主布局
self.main_layout.addWidget(item_widget, alignment=Qt.AlignTop)
def _create_task_item(self, task_name, task_id, status_icon):
"""创建单条任务条目, 返回任务条目和控件字典"""
item_widget = QWidget()
item_layout = QVBoxLayout(item_widget)
item_layout.setContentsMargins(9, 6, 9, 8)
item_layout.setSpacing(0)
# 相应的 任务条目的控件字典
controls = {} # {"volume_label": ..., "time_label": ..., ...}
# 水平布局1选择按钮 + 任务名 + 详情按钮
row1_layout = QHBoxLayout()
# 任务选择按钮
select_btn = QPushButton()
select_btn.setFixedSize(14, 14)
select_btn.setCursor(Qt.PointingHandCursor)
select_btn.setStyleSheet("""
QPushButton {
background-image: url(:/icons/images/任务信息选择按钮1.png);
border: none;
}
QPushButton:checked {
background-image: url(:/icons/images/任务信息选择按钮2.png);
}
""")
select_btn.setCheckable(True)
controls["select_btn"] = select_btn
row1_layout.addWidget(select_btn)
# 任务编号
task_id_label = QLabel(task_id)
task_id_label.setStyleSheet("font-size: 18px; color: #16ffff;padding-left: 6px;")
controls["task_id_label"] = task_id_label
row1_layout.addWidget(task_id_label)
# 详情按钮
detail_btn = QPushButton()
detail_btn.setText("详情")
detail_btn.setFixedSize(46, 26)
detail_btn.setCursor(Qt.PointingHandCursor)
detail_btn.setStyleSheet("""
QPushButton {
background-image: url(:/icons/images/任务信息详情按钮1.png);
border: none;
color: #3bfff8;
font-size: 16px;
}
QPushButton:hover {
background-image: url(:/icons/images/任务信息详情按钮2.png);
color: #001c83;
font-size: 16px;
}
""")
detail_btn.clicked.connect(lambda: self._show_detail_dialog(task_name)) # 详情按钮槽函数
controls["detail_btn"] = detail_btn
row1_layout.addWidget(detail_btn)
item_layout.addLayout(row1_layout)
# 水平布局2方量 + / + 时间 + 状态图标
row2_layout = QHBoxLayout()
# 方量标签
volume_label = QLabel("方量 200")
volume_label.setStyleSheet("color: #a1c1d7; font-size: 14px;padding-left: 19px;")
controls["volume_label"] = volume_label
row2_layout.addWidget(volume_label)
# 分隔标签
sep_label = QLabel("/")
sep_label.setStyleSheet("color: #a1c1d7;")
row2_layout.addWidget(sep_label, alignment=Qt.AlignCenter)
# 时间标签
time_label = QLabel("03:22PM")
time_label.setStyleSheet("color: #a1c1d7; font-size: 14px;")
controls["time_label"] = time_label
row2_layout.addWidget(time_label)
# 状态标签
status_icon_label = QLabel()
status_icon_label.setPixmap(QPixmap(status_icon))
controls["status_icon_label"] = status_icon_label
row2_layout.addWidget(status_icon_label, alignment=Qt.AlignRight)
item_layout.addLayout(row2_layout)
# 分隔线
item_layout.setSpacing(5)
separator = QLabel()
separator.setPixmap(QPixmap(":/icons/images/任务信息分隔.png"))
separator.setFixedSize(196, 1)
item_layout.addWidget(separator)
return item_widget, controls # 返回任务条目 以及 相应的控件
def _show_detail_dialog(self, task_name):
"""显示任务详情弹窗"""
QMessageBox.information(self, "任务详情", f"任务 {task_name} 的详细信息...")
# --------------------------
# 对外接口:修改任务属性
# 三个任务条目对应的任务名task_name分别为 task1、task2、task3
# --------------------------
def set_task_volume(self, task_name:str, volume: float):
"""修改指定任务的方量, 传入具体的方量值,如: 200.0"""
if task_name in self.task_controls:
volume_label = self.task_controls[task_name]["volume_label"]
volume_label.setText(f"方量 {volume}")
def set_task_time(self, task_name:str, time_str: str):
"""修改指定任务的时间, 传入对应格式的时间,如: 03:22PM"""
if task_name in self.task_controls:
time_label = self.task_controls[task_name]["time_label"]
time_label.setText(time_str)
def set_task_id(self, task_name:str, new_id: str):
"""修改指定任务的编号, 如: SHRB2-3"""
if task_name in self.task_controls:
task_id_label = self.task_controls[task_name]["task_id_label"]
task_id_label.setText(new_id)
if __name__ == "__main__":
app = QApplication(sys.argv)
widget = TaskWidget("管片任务")
# 示例修改task2的方量为300测试用
widget.set_task_volume("task2", 300)
# 示例修改task1的时间为04:50PM测试用
widget.set_task_time("task1", "04:50PM")
widget.show()
sys.exit(app.exec())

View File

@ -0,0 +1,145 @@
from PySide6.QtWidgets import (QWidget, QHBoxLayout, QPushButton, QLineEdit,
QApplication)
from PySide6.QtCore import Qt
from PySide6.QtGui import QDoubleValidator
import sys
# 调整计划方量
class ValueAdjuster(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.min_value = 0 # 最小值
self.max_value = 99 # 最大值
self.value = 2.5 # 初始值
self.setFixedSize(102, 32)
# 创建控件
# 减号按钮
self.minus_btn = QPushButton("-")
self.minus_btn.setFixedSize(26, 26)
self.minus_btn.setCursor(Qt.PointingHandCursor)
# 中间的编辑栏
self.line_edit = QLineEdit(f"{self.value:.1f}") # 显示1位小数
self.line_edit.setFixedSize(40, 26)
# 加号按钮
self.plus_btn = QPushButton("+")
self.plus_btn.setFixedSize(26, 26)
self.plus_btn.setCursor(Qt.PointingHandCursor)
# 配置QLineEdit支持数字输入+居中显示
self.line_edit.setAlignment(Qt.AlignCenter) # 文本居中
# 限制输入为浮点数(支持负数,范围可自定义)
self.line_edit.setValidator(QDoubleValidator(0, 99, 1, self)) # 最多1位小数
self.line_edit.textChanged.connect(self.on_text_changed) # 监听输入变化
# 设置样式表(保持与按钮风格统一)
self.minus_btn.setStyleSheet("""
QPushButton {
background-color: #001c83;
color: #03f5ff;
border: 1px solid #039ec3;
font-size: 29px;
text-align: center;
padding-bottom: 4px;
}
QPushButton:hover {
background-color: #16ffff;
color: #00347e;
}
""")
# #03f5ff
self.line_edit.setStyleSheet("""
QLineEdit {
background-color: #001c83;
color: white;
border: 1px solid #039ec3;
font-size: 16px;
padding:0px;
font-weight: 560;
}
QLineEdit:focus {
border: 1px solid #039ec3;
outline: none;
}
""")
self.plus_btn.setStyleSheet("""
QPushButton {
background-color: #001c83;
color: #03f5ff;
border: 1px solid #039ec3;
font-size: 29px;
text-align: center;
padding-bottom: 4px;
margin: 0px;
}
QPushButton:hover {
background-color: #16ffff;
color: #00347e;
}
""")
# 连接信号槽(加减按钮)
self.minus_btn.clicked.connect(self.on_minus_clicked)
self.plus_btn.clicked.connect(self.on_plus_clicked)
# 设置布局间距0确保边框无缝衔接
layout = QHBoxLayout(self)
layout.setSpacing(2)
layout.setContentsMargins(0, 0, 0, 0) # 去除布局外边距
layout.addWidget(self.minus_btn)
layout.addWidget(self.line_edit)
layout.addWidget(self.plus_btn)
def on_minus_clicked(self):
"""减0.1"""
new_value = self.value - 0.1
# 限制最小值不低于min_value
if new_value >= self.min_value:
self.value = round(new_value, 1)
self.line_edit.setText(f"{self.value:.1f}")
def on_plus_clicked(self):
"""加0.1"""
new_value = self.value + 0.1
# 限制最大值不超过max_value
if new_value <= self.max_value:
self.value = round(new_value, 1)
self.line_edit.setText(f"{self.value:.1f}")
def on_text_changed(self, text):
"""监听输入框文本变化更新内部value"""
if not text:
return
try:
input_value = round(float(text), 1)
# 确保输入值在范围内
if self.min_value <= input_value <= self.max_value:
self.value = input_value
else:
# 超出范围时恢复到最近的有效 value
self.line_edit.setText(f"{self.value:.1f}")
except ValueError:
pass
# 获取具体的方量数值
def get_value(self):
return self.value
# 设置方量值
def set_value(self, value:float):
input_value = round(float(value), 1)
# 确保设置的值在范围内
if self.min_value <= input_value <= self.max_value:
self.value = input_value
self.line_edit.setText(f"{self.value:.1f}") # 更新方量值
if __name__ == "__main__":
app = QApplication(sys.argv)
widget = ValueAdjuster()
widget.show()
sys.exit(app.exec())

View File

@ -1,7 +1,17 @@
# 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 PySide6.QtGui import QImage, QPixmap, QPalette, QBrush, QCursor, QIcon, QPainter, QFont
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
QHBoxLayout,
QLabel,
QApplication,
QFrame,
QPushButton,
QStackedWidget,
QSizePolicy
)
from typing import Optional
import cv2
@ -9,12 +19,23 @@ import time
from threading import Thread, Lock
import sys
from .switch_button import SwitchButton
# ====================== 后台视频流管理(自动重连)======================
class VideoStream:
"""后台读取 RTSP 流,自动重连,只返回新鲜帧"""
import resources.resources_rc
def __init__(self, rtsp_url, name="Stream"):
class VideoStream(QObject):
# 系统(信息)信号
info_signal = Signal(str, str)
# 警告信号(重试): 发送摄像头名称和状态描述
warning_signal = Signal(str, str)
# 异常错误信号(超次数)
error_signal = Signal(str, str)
def __init__(self, rtsp_url, name="Stream", max_retry=3):
super().__init__()
self.rtsp_url = rtsp_url
self.name = name
self.cap = None
@ -24,40 +45,103 @@ class VideoStream:
self.running = False
self.thread = None
self.max_retry = max_retry
self.retry_count = 0
self.manual_stop = False # 是否停止自动重连
def start(self):
self.running = True
self.thread = Thread(target=self.update, args=(), daemon=True)
self.thread = Thread(target=self.update, daemon=True)
self.thread.start()
return self
def reset_retry(self):
with self.lock:
self.retry_count = 0
self.manual_stop = False
if self.cap:
self.cap.release()
self.cap = None
self.info_signal.emit(self.name, "已手动重置, 开始尝试连接...")
def _handle_max_retry(self, failure_type: str):
"""
处理超过最大重试次数的情况
failure_type: 失败类型("连接""读取"
"""
self.manual_stop = True
error_msg = f"{failure_type}摄像头失败, 已经超过最大次数({self.max_retry}次),可能设备故障,请检查后手动点击重连"
print(f"[{self.name}] {error_msg}")
self.error_signal.emit(self.name, error_msg)
time.sleep(1)
def _handle_retry_delay(self):
"""计算并执行重试前的延迟(递增间隔)"""
sleep_time = min(0.5 + self.retry_count * 0.5, 5)
warning_msg = (
f"{self.retry_count}次连接失败,将在{sleep_time:.1f}秒后再次尝试..."
)
self.warning_signal.emit(self.name, warning_msg)
print(f"[{self.name}] {warning_msg}")
time.sleep(sleep_time)
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}")
if self.manual_stop:
time.sleep(1)
continue
if self.cap is None or not self.cap.isOpened():
print(
f"[{self.name}]正在连接 RTSP(第{self.retry_count+1}次): {self.rtsp_url}"
)
info_msg = f"正在连接 RTSP(第{self.retry_count+1}次): {self.rtsp_url}"
self.info_signal.emit(self.name, info_msg)
try:
self.cap = cv2.VideoCapture(self.rtsp_url, cv2.CAP_FFMPEG)
if hasattr(cv2, "CAP_PROP_BUFFERSIZE"):
self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 2)
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 ex:
print(f"[{self.name}]连接失败: {ex}")
warning_msg = f"连接失败: {str(ex)},准备重试..."
self.warning_signal.emit(self.name, warning_msg)
if self.cap:
self.cap.release()
self.cap = None
if self.cap is None or not self.cap.isOpened():
self.retry_count += 1
if self.retry_count >= self.max_retry:
self._handle_max_retry("连接")
continue
self._handle_retry_delay()
continue
else:
self.retry_count = 0
self.info_signal.emit(self.name, "连接成功")
ret, frame = self.cap.read()
if ret:
with self.lock:
self.frame = frame.copy()
self.timestamp = time.time()
self.retry_count = 0
else:
print(f"[{self.name}] 读取失败,准备重连...")
warning_msg = "读取帧失败,准备重连..."
self.warning_signal.emit(self.name, warning_msg)
if self.cap:
self.cap.release()
self.cap = None
time.sleep(1)
self.retry_count += 1
if self.retry_count >= self.max_retry:
self._handle_max_retry("读取")
continue
self._handle_retry_delay()
def read(self):
with self.lock:
@ -75,102 +159,307 @@ class VideoStream:
print(f"[{self.name}] 视频流已停止")
# ====================== 摄像头功能模块 ======================
# ====================== 摄像头功能模块 ======================
class CameraModule(QWidget):
"""单个摄像头模块:原图显示"""
def __init__(self, camera_name="摄像头", rtsp_url="", need_rotate_180=True, parent=None):
# 重置信号,用于通知需要重置连接 (重连)
reset_signal = Signal()
# 视频显示信号,用于通知是否显示视频(刷新视频帧)
# 发送摄像头名 和 是否显示
video_display_signal = Signal(str, bool)
def __init__(
self, camera_name="摄像头", rtsp_url="", need_rotate_180=True, parent=None
):
super().__init__(parent)
self.setObjectName("cameraModule")
self.camera_name = camera_name
self.rtsp_url = rtsp_url
self.need_rotate_180 = need_rotate_180 # 画面是否需要旋转180度后显示
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. 标题 ---
# --- 0. 标题布局: 标题 + 显示标签及开关 ---
title_layout = QHBoxLayout()
title_layout.setContentsMargins(0, 0, 0, 4)
title_layout.setSpacing(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("""
self.title_label.setStyleSheet(
"""
#cameraTitleLabel {
font-size: 16px;
color: black;
font-weight: bold;
font-size: 18px;
color: #16ffff;
background-image: url(":/icons/images/视频标题背景.png");
min-width: 180px;
padding-left: 12px;
}
"""
)
# 创建【显示标签+开关】组合容器
self.display_group = QWidget() # 容器:包裹标签和开关
display_group_layout = QHBoxLayout(self.display_group)
display_group_layout.setContentsMargins(0, 0, 0, 0) # 容器内边距为0
display_group_layout.setSpacing(0)
# 显示标签
self.display_label = QLabel("显示")
self.display_label.setObjectName("displayLabel")
# font-weight: bold;
self.display_label.setStyleSheet("""
#displayLabel {
font-size: 18px;
color: #16ffff;
margin: 0px;
padding: 0px;
}
""")
# 让标签垂直居中
self.display_label.setFixedWidth(40)
self.display_label.setAlignment(Qt.AlignVCenter | Qt.AlignLeft)
# --- 1. 原始图像 ---
self.raw_label = QLabel() # 显示图像的 label
# self.raw_label.setFixedSize(320, 240)
self.raw_label.setFixedSize(327, 199) # 显示的图像的宽、高
# 显示开关
self.display_switch = SwitchButton()
self.display_switch.setChecked(True)
self.display_switch.switched.connect(self.onDisplayButtonSwitched)
palette = self.raw_label.palette()
palette.setColor(self.raw_label.foregroundRole(), Qt.white) # 字体颜色:白色
self.raw_label.setPalette(palette)
# 将显示标签和开关添加到组合容器布局
display_group_layout.addWidget(self.display_label)
display_group_layout.addWidget(self.display_switch, alignment=Qt.AlignLeft)
# 添加到标题布局
title_layout.addWidget(self.title_label, alignment=Qt.AlignLeft)
title_layout.addWidget(self.display_group, alignment=Qt.AlignLeft)
# 视频显示容器:使用堆叠布局,让重连按钮和视频标签分层显示
self.video_container = QWidget()
self.video_container.setObjectName("videoContainer")
self.video_container.setFixedSize(327, 199) # 需要同步修改下面的尺寸
# #011d6b #033474
# self.video_container.setStyleSheet(
# "background-color: #011d6b; border: none;"
# ) # 明确视频显示背景色
self.video_container.setStyleSheet(
"""
#videoContainer {
border-image: url(":/icons/images/视频框背景.png");
}
"""
)
video_layout = QVBoxLayout(self.video_container)
video_layout.setContentsMargins(6, 6, 5, 6)
# 原始图像显示
self.raw_label = QLabel()
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.setStyleSheet("border: none;") # 移除背景,使用容器背景
# self.raw_label.setStyleSheet("background-color: red;")
self.raw_label.setText(f"{self.camera_name}摄像头, 连接中...")
# self.raw_label.setFixedSize(327, 199)
# self.raw_label.setFixedSize(314, 187) # 需要根据视频框背景大小调整
self.raw_label.setFixedSize(316, 187) # 需要根据视频框背景大小调整
# --- 添加到主布局 ---
layout.addWidget(self.title_label) # 标题
layout.addWidget(self.raw_label) # 图像
# 重置按钮:居中显示、透明背景
self.reset_button = QPushButton()
# self.reset_button.setFixedSize(327, 199)
# self.reset_button.setFixedSize(314, 187) # 同上
self.reset_button.setFixedSize(316, 187) # 同上
self.reset_button.setStyleSheet(
"""
QPushButton {
background-color: transparent;
border: none;
padding: 0px;
}
"""
)
self.reset_button.setCursor(QCursor(Qt.PointingHandCursor))
self.reset_button.clicked.connect(self.onRestButtonClicked)
# 加载视频重新连接图片
try:
pixmap = QPixmap(":/icons/images/界面刷新.png")
if not pixmap.isNull():
self.reset_button.setIcon(QIcon(pixmap))
self.reset_button.setIconSize(pixmap.size())
else:
# 图片加载失败(文件不存在或格式错误)
self.reset_button.setText("点击重置连接")
except Exception as e:
print(f"加载复位图片失败: {e}")
self.reset_button.setText("点击重置连接")
# 堆叠布局:重连按钮和视频标签分层
self.stacked_widget = QStackedWidget()
self.stacked_widget.addWidget(self.raw_label)
self.stacked_widget.addWidget(self.reset_button)
video_layout.addWidget(self.stacked_widget, alignment=Qt.AlignCenter)
# 添加到主布局
layout.addLayout(title_layout)
layout.addWidget(self.video_container)
# 显示开关切换槽函数
def onDisplayButtonSwitched(self, state:bool):
# 显示开关打开state 为 True显示开关关闭state 为 False
if state:
self.stacked_widget.setCurrentWidget(self.raw_label) # 视频显示标签
self.stacked_widget.setHidden(False)
else:
self.stacked_widget.setHidden(True)
self.video_display_signal.emit(self.camera_name, state)
# 重连按钮点击槽函数
def onRestButtonClicked(self):
self.stacked_widget.setCurrentWidget(self.raw_label)
self.raw_label.setText(f"{self.camera_name}摄像头, 连接中...")
self.reset_signal.emit() # 发送重连信号
def showResetButton(self):
self.raw_label.setText("")
self.stacked_widget.setCurrentWidget(self.reset_button)
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.setPixmap(
pixmap.scaled(
self.raw_label.size(),
Qt.IgnoreAspectRatio, # 忽略比例,占满窗口 可选Qt.KeepAspectRatio
Qt.SmoothTransformation,
)
)
self.raw_label.setText("")
else:
self.raw_label.setPixmap(QPixmap())
self.raw_label.setText(f"{self.camera_name}摄像头, 断线中...")
# 没有图片数据stream流会自动重连 或 手动重连
self.raw_label.setText(f"{self.camera_name}摄像头, 连接中...")
# 视频显示区域 总的标题 (如 振捣视频)
class VideoTitleWidget(QWidget):
def __init__(self, video_name, parent=None):
super().__init__(parent)
self.setStyleSheet("background-color: transparent;") # 整体透明
self.setup_ui(video_name)
def setup_ui(self, video_name):
# 水平布局:文字 + 图标
layout = QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0) # 布局内边距为0
# 文字标签
self.text_label = QLabel(f"{video_name}视频")
self.text_label.setStyleSheet("""
font-family: "微软雅黑";
font-size: 20px;
font-weight: bold;
color: #16FFFF;
background-color: transparent;
""")
font = self.text_label.font()
font.setLetterSpacing(QFont.SpacingType.AbsoluteSpacing, 3) # 字间距3px可按需加大
self.text_label.setFont(font)
# 图标标签
self.icon_label = QLabel()
self.icon_label.setPixmap(QPixmap(":/icons/images/视频摄像头标志.png")) # 需要换为实际的图片的路径
self.icon_label.setStyleSheet("background-color: transparent;")
self.icon_label.setFixedSize(23, 16)
self.icon_label.setScaledContents(True)
# 添加到布局
layout.addWidget(self.text_label, alignment=Qt.AlignCenter)
layout.addWidget(self.icon_label, alignment=Qt.AlignCenter)
class VibrationVideoWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setObjectName("vibrationVideoWidget")
# 1. 加载背景图片(替换为你的图片路径)
self.background_image = QImage() # 初始化图片对象
# 注意:图片路径需正确,建议使用绝对路径或项目相对路径
self.background_path = ":/icons/images/视频背景.png" # 你的背景图片路径
if not self.background_image.load(self.background_path):
# 图片加载失败时的容错处理(显示红色背景作为提示)
print(f"警告:无法加载背景图片 {self.background_path}")
self.background_image = QImage(388, 839, QImage.Format_RGB32)
self.background_image.fill(Qt.GlobalColor.red) # 加载失败时用红色填充
# 显示摄像头画面的模组
# 需要修改为相应的地址!!!
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")
# 注在camera_controller中设置地址url
self.cam1 = CameraModule("位料斗")
self.cam2 = CameraModule("下位料斗")
self.cam3 = CameraModule("模具车")
self.setup_ui()
self.init_streams()
self.connect_signals()
def set_camera_urls(self, cam1_url, cam2_url, cam3_url):
"""设置三个摄像头的RTSP URL
注意: 在CameraController中调用
"""
self.cam1.rtsp_url = cam1_url
self.cam2.rtsp_url = cam2_url
self.cam3.rtsp_url = cam3_url
def setup_ui(self):
# 视频widget样式
self.setFixedSize(387, 720) # 宽387、高792
self.setAutoFillBackground(True)
self.setStyleSheet("""
#vibrationVideoWidget {
background-color: #043d76;
border: none; /* 可选:去除默认边框,避免视觉干扰 */
}
""")
# self.setFixedSize(387, 720) # 宽387、高792
self.setFixedSize(388, 839) # 背景图片宽388、高879
# 1、总的标题
self.title_widget = VideoTitleWidget("振捣")
# 布局设置
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)
# 2. 创建摄像头容器专门存放cam1/cam2/cam3
self.camera_container = QWidget() # 容器widget
self.camera_container.setFixedSize(350, 720) # 容器固定尺寸
# 给容器添加样式
self.camera_container.setStyleSheet("""
background-color: transparent;
border: none;
""")
# 3. 容器内部布局(管理三个摄像头模块)
container_layout = QVBoxLayout(self.camera_container)
container_layout.setContentsMargins(6, 6, 6, 6) # 容器内边距
container_layout.setSpacing(6) # 摄像头模块之间的间距
# 将三个摄像头添加到容器布局中
container_layout.addWidget(self.cam1, alignment=Qt.AlignCenter)
container_layout.addWidget(self.cam2, alignment=Qt.AlignCenter)
container_layout.addWidget(self.cam3, alignment=Qt.AlignCenter)
# 4. 主窗口布局(管理摄像头容器等其他部件)
main_layout = QVBoxLayout(self) # 主窗口的布局
main_layout.setContentsMargins(0, 0, 9, 30) # 主布局内边距为0
# 添加标题部件
main_layout.addWidget(self.title_widget, alignment=Qt.AlignCenter) # 标题居中
# 添加43px间距标题与摄像头容器之间
main_layout.addSpacing(16)
# 把摄像头容器添加到主布局中(作为一个整体)
main_layout.addWidget(self.camera_container, alignment=Qt.AlignBottom | Qt.AlignHCenter)
# 初始化视频流 (注意设置好了camera_urls之后调用)
def init_streams(self):
url1 = self.cam1.rtsp_url
url2 = self.cam2.rtsp_url
@ -182,20 +471,55 @@ class VibrationVideoWidget(QWidget):
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
# 流Stream相关的槽函数连接放在了 camera_controller之中
# 视频显示相关的槽函数连接
self.cam1.video_display_signal.connect(self.onVideoDisplay)
self.cam2.video_display_signal.connect(self.onVideoDisplay)
self.cam3.video_display_signal.connect(self.onVideoDisplay)
def onVideoDisplay(self, cam_name: str, state: bool):
# 1. 摄像头与 stream、timer 的映射关系
cam_mapping = {
"上位料斗": (self.stream1, self.timer1),
"下位料斗": (self.stream2, self.timer2),
"模具车": (self.stream3, self.timer3)
}
# 2. 获取当前摄像头对应的 stream 和 timer处理无效名称的情况
stream, timer = cam_mapping.get(cam_name, (None, None))
if not stream or not timer:
print(f"警告:未知摄像头名称 {cam_name}")
return
# 3. 根据 state 执行操作
if state: # 显示视频
stream.reset_retry() # 视频流重连
timer.start(33) # 定时器开始,显示视频帧
else: # 关闭视频
timer.stop() # 定时器停止
stream.manual_stop = True # 视频流停止读取
def onStreamError(self, stream_name):
if stream_name == "Cam1":
self.cam1.showResetButton()
elif stream_name == "Cam2":
self.cam2.showResetButton()
elif stream_name == "Cam3":
self.cam3.showResetButton()
def update_frame1(self):
frame = self.stream1.read()
@ -208,7 +532,7 @@ class VibrationVideoWidget(QWidget):
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:
@ -233,19 +557,43 @@ class VibrationVideoWidget(QWidget):
else:
self.cam3.update_raw_image(None)
def paintEvent(self, event):
# 1. 先调用父类的paintEvent
super().paintEvent(event)
# 2. 创建画家对象,绘制背景图片
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) # 图片缩放平滑
# 3. 获取部件的实际尺寸这里是387x720与setup_ui中一致
widget_rect = self.rect()
# 4. 绘制背景图片(缩放至部件大小)
# 可选缩放策略:
# - Qt.KeepAspectRatio保持图片比例可能有黑边
# - Qt.IgnoreAspectRatio拉伸填充可能变形
# - Qt.KeepAspectRatioByExpanding按比例放大至覆盖部件可能裁剪边缘
scaled_pixmap = QPixmap.fromImage(self.background_image).scaled(
widget_rect.size(),
Qt.KeepAspectRatio, # 推荐:保持比例,避免变形
Qt.SmoothTransformation # 平滑缩放
)
# 5. 计算图片绘制位置(居中显示,若有黑边则均匀分布)
x = (widget_rect.width() - scaled_pixmap.width()) // 2
y = (widget_rect.height() - scaled_pixmap.height()) // 2
painter.drawPixmap(x, y, scaled_pixmap)
# 6. 结束绘制
painter.end()
# ====================== 清理资源 ======================
def closeEvent(self, event):
self.hide()
self.stream1.stop()
self.stream2.stop()
self.stream3.stop()
self.timer1.stop()
self.timer2.stop()
self.timer3.stop()
self.stream1.stop()
self.stream2.stop()
self.stream3.stop()
super().closeEvent(event)
if __name__ == "__main__":
app = QApplication([])
window = VibrationVideoWidget()
window.show()
sys.exit(app.exec())