Files
Feeding_control_system/view/widgets/system_diagnostics_dialog.py

386 lines
14 KiB
Python
Raw Normal View History

2025-11-07 15:45:54 +08:00
from PySide6.QtWidgets import (
QApplication,
QMainWindow,
QWidget,
QVBoxLayout,
QGridLayout,
QLabel,
QHBoxLayout,
QListWidget,
QListWidgetItem,
QSpacerItem,
QSizePolicy,
QLineEdit,
QDialog,
)
from PySide6.QtGui import QPixmap, QFont, QColor, QTransform, QPainter
from PySide6.QtCore import (
Qt,
QPoint,
QEvent,
QPropertyAnimation,
QEasingCurve,
QRect,
QParallelAnimationGroup,
)
import sys
from utils.image_paths import ImagePaths
"""
系统诊断按钮的弹窗: 可以显示设备的状态
"""
2025-11-07 15:45:54 +08:00
class CustomDropdown(QWidget):
"""自定义下拉框组件"""
def __init__(self, options, arrow_img_path, parent=None):
super().__init__(parent)
self.options = options
self.arrow_img_path = arrow_img_path
self.is_expanded = False
# 主布局(标签 + 箭头)
self.main_layout = QHBoxLayout(self)
self.main_layout.setContentsMargins(0, 0, 0, 0)
self.main_layout.setSpacing(0)
self.main_layout.setAlignment(Qt.AlignLeft)
self.setFixedSize(63, 19) # 需要根据下拉框需要显示的文字来修改
# self.setFixedHeight(19)
# 1. 结果显示标签QLabel无clicked信号
self.result_label = QLabel(options[0])
self.result_label.setStyleSheet(
"""
background-image: url("");
color: #16ffff;
background-color: transparent;
border: none;
padding: 0px;
font-size: 18px;
"""
)
# self.result_label.setCursor(Qt.PointingHandCursor) # 手型光标提示可点击
self.main_layout.addWidget(
self.result_label, alignment=Qt.AlignVCenter | Qt.AlignLeft
)
# 2. 可点击的箭头标签QLabel
self.arrow_label = QLabel()
self.arrow_pixmap = QPixmap(arrow_img_path)
self.arrow_label.setStyleSheet("background-image: url(" ");")
self.arrow_label.setPixmap(
self.arrow_pixmap.scaled(12, 9, Qt.KeepAspectRatio, Qt.SmoothTransformation)
)
self.arrow_label.setCursor(Qt.PointingHandCursor)
self.main_layout.addWidget(self.arrow_label, alignment=Qt.AlignTop)
# 3. 下拉选项列表(默认选中第一个)
self.list_widget = QListWidget()
self.list_widget.setWindowFlags(Qt.Popup)
# 设置选项字体
font = QFont()
font.setPixelSize(16)
# 添加所有的下拉选项
for option in options:
item = QListWidgetItem(option)
item.setTextAlignment(Qt.AlignLeft)
item.setFont(font)
self.list_widget.addItem(item)
self.list_widget.setCurrentRow(0) # 默认选中第一项
self.list_widget.itemClicked.connect(self.select_option)
# 双保险监听:全局焦点变化 + 事件过滤
self.app = QApplication.instance()
self.app.focusChanged.connect(self.on_focus_changed)
self.list_widget.installEventFilter(self)
def mousePressEvent(self, event):
"""重写鼠标点击事件实现QLabel点击功能"""
# 判断点击是否在result_label或arrow_label区域内
# if self.result_label.underMouse() or self.arrow_label.underMouse():
# self.toggle_expand()
if self.arrow_label.underMouse():
self.toggle_expand()
super().mousePressEvent(event) # 传递事件,不影响其他组件
def toggle_expand(self):
"""切换下拉框展开/收起 + 箭头旋转"""
if self.is_expanded:
self.list_widget.hide()
# 箭头恢复向下
self.arrow_label.setPixmap(self.arrow_pixmap)
2025-11-07 15:45:54 +08:00
else:
# 计算下拉框位置(在标签下方对齐)
label_pos = self.result_label.mapToGlobal(
QPoint(0, self.result_label.height())
)
self.list_widget.setGeometry(
label_pos.x(), label_pos.y(), self.result_label.width() + 10, 80
)
self.list_widget.show()
self.list_widget.setFocus()
# 箭头旋转180度向上
transform = QTransform().rotate(180)
rotated_pixmap = self.arrow_pixmap.transformed(
transform, Qt.SmoothTransformation
)
self.arrow_label.setPixmap(rotated_pixmap)
self.is_expanded = not self.is_expanded
def select_option(self, item):
"""选择选项后更新标签 + 收起下拉框"""
self.result_label.setText(item.text())
self.list_widget.hide()
self.arrow_label.setPixmap(self.arrow_pixmap)
self.is_expanded = False
def on_focus_changed(self, old_widget, new_widget):
"""焦点变化时关闭下拉框"""
if self.is_expanded:
is_focus_on_self = (
new_widget == self
or new_widget == self.result_label
or new_widget == self.arrow_label
or (self.list_widget.isAncestorOf(new_widget) if new_widget else False)
)
if not is_focus_on_self:
self.list_widget.hide()
self.arrow_label.setPixmap(self.arrow_pixmap)
self.is_expanded = False
def eventFilter(self, obj, event):
"""点击外部关闭下拉框"""
if obj == self.list_widget and event.type() == QEvent.MouseButtonPress:
self.list_widget.hide()
self.arrow_label.setPixmap(
self.arrow_pixmap.scaled(
12, 9, Qt.KeepAspectRatio, Qt.SmoothTransformation
)
)
self.is_expanded = False
return True
return super().eventFilter(obj, event)
def setFont(self, font):
"""设置字体"""
self.result_label.setFont(font)
for i in range(self.list_widget.count()):
self.list_widget.item(i).setFont(font)
# 获取当前选中的设备名
def get_selected_device(self):
return self.result_label.text()
class SystemDiagnosticsDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setAttribute(Qt.WA_TranslucentBackground)
self.setWindowOpacity(0.0)
self._init_ui()
self.init_animations()
def _init_ui(self):
# 无边框模式
self.setWindowFlags(Qt.FramelessWindowHint)
# 加载系统诊断弹窗的背景图片
self.bg_pixmap = QPixmap(ImagePaths.SYSTEM_DIAGNOSTICS_POPUP_BG)
if self.bg_pixmap.isNull():
print("错误: 系统诊断弹窗背景图加载失败!请检查路径是否正确")
else:
# 窗口尺寸与图片尺寸完全一致
self.setFixedSize(self.bg_pixmap.size())
# 网格布局8行4列小框
grid_layout = QGridLayout(self)
grid_layout.setContentsMargins(24, 28, 20, 24)
# 图片路径(替换为实际路径)
box_image_path = ImagePaths.SYSTEM_DIAGNOSTICS_BOX
circle_normal_path = ImagePaths.SYSTEM_DIAGNOSTICS_STATUS_GREEN # 正常状态
circle_warning_path = ImagePaths.SYSTEM_DIAGNOSTICS_STATUS_YELLOW # 警告状态
circle_error_path = ImagePaths.SYSTEM_DIAGNOSTICS_STATUS_RED # 异常状态
ms_box_path = ImagePaths.SYSTEM_DIAGNOSTICS_MS_BG
dropdown_arrow_path = ImagePaths.SYSTEM_DIAGNOSTICS_DROPDOWN_ARROW
# 字体设置
ms_font = QFont()
ms_font.setPixelSize(14)
ms_color = QColor("#14abea")
# 生成小框
for row in range(8):
for col in range(4):
box_container = QWidget()
box_container.setObjectName(f"box_{row}_{col}")
box_container.setStyleSheet(
f"""
background-image: url("{box_image_path}");
background-repeat: no-repeat;
"""
)
box_layout = QHBoxLayout(box_container)
box_layout.setSpacing(0)
# ========== 状态圆圈(支持状态切换) ==========
circle_label = QLabel()
circle_label.status = "normal"
circle_label.pixmaps = {
"normal": QPixmap(circle_normal_path),
"warning": QPixmap(circle_warning_path),
"error": QPixmap(circle_error_path),
}
circle_label.setPixmap(circle_label.pixmaps["normal"])
circle_label.setStyleSheet("background: none;")
# ========== 自定义下拉框(支持获取设备名) ==========
led_dropdown = CustomDropdown(
options=["LED1", "LED2", "LED3"], arrow_img_path=dropdown_arrow_path
)
# ========== 秒数输入框(获取毫秒值) ==========
ms_container = QWidget()
ms_layout = QHBoxLayout(ms_container)
ms_layout.setContentsMargins(6, 0, 0, 0)
ms_edit = QLineEdit("5ms")
ms_edit.setFont(ms_font)
ms_edit.setStyleSheet(
f"""
background: none;
color: {ms_color.name()};
border: none;
outline: none;
background-color: transparent;
"""
)
ms_container.setStyleSheet(
f"""
background-image: url("{ms_box_path}");
background-repeat: no-repeat;
"""
)
ms_layout.addWidget(ms_edit)
# 保存组件引用 (动态增加)
box_container.circle = circle_label
box_container.dropdown = led_dropdown
box_container.ms_edit = ms_edit
# 间距调整
spacer1 = QSpacerItem(5, 1, QSizePolicy.Fixed, QSizePolicy.Minimum)
spacer2 = QSpacerItem(5, 1, QSizePolicy.Fixed, QSizePolicy.Minimum)
spacer3 = QSpacerItem(8, 1, QSizePolicy.Fixed, QSizePolicy.Minimum)
# box_layout.addItem(spacer1)
box_layout.addWidget(circle_label)
box_layout.addItem(spacer2)
box_layout.addWidget(led_dropdown)
# box_layout.addItem(spacer3)
box_layout.addWidget(ms_container)
grid_layout.addWidget(box_container, row, col)
def init_animations(self):
"""初始化显示动画:从下方滑入 + 淡入"""
# 1. 透明度动画从0→1与系统中心一致但时长不同
self.opacity_anim = QPropertyAnimation(self, b"windowOpacity")
self.opacity_anim.setDuration(400)
self.opacity_anim.setStartValue(0.0)
self.opacity_anim.setEndValue(1.0)
self.opacity_anim.setEasingCurve(QEasingCurve.OutCubic) # 缓动曲线不同
# 2. 位置动画从下方100px滑入目标位置核心差异点
self.pos_anim = QPropertyAnimation(self, b"geometry")
self.pos_anim.setDuration(400)
self.pos_anim.setEasingCurve(QEasingCurve.OutQuart) # 滑入效果更自然
# 3. 组合动画(同时执行滑入和淡入)
self.anim_group = QParallelAnimationGroup(self)
self.anim_group.addAnimation(self.opacity_anim)
self.anim_group.addAnimation(self.pos_anim)
def showEvent(self, event):
super().showEvent(event) # 先调用父类方法
# 动态计算动画起点在当前位置下方100px保持宽度和高度不变
current_geometry = self.geometry() # 当前位置和尺寸需提前用move设置
# 起点y坐标增加100px从下方滑入x和尺寸不变
start_rect = QRect(
current_geometry.x(),
current_geometry.y() + 100, # 下方100px
current_geometry.width(),
current_geometry.height()
)
# 设置动画起点和终点
self.pos_anim.setStartValue(start_rect)
self.pos_anim.setEndValue(current_geometry) # 终点:目标位置
# 启动动画
self.anim_group.start()
def paintEvent(self, event):
"""重写绘制事件,手动在透明背景上绘制图片"""
if not self.bg_pixmap.isNull():
painter = QPainter(self)
# 绘制背景图(完全覆盖窗口,无间隙)
painter.drawPixmap(self.rect(), self.bg_pixmap)
# 必须调用父类方法,确保子控件正常绘制
super().paintEvent(event)
"""
注意: row表示行号col表示列号都是从 0开始, 比如: 0行0列
"""
# ========== 对外接口:设置设备状态 ==========
def set_circle_status(self, row, col, status):
"""设置指定行列的状态(绿-黄-红) (normal/warning/error)"""
box = self.findChild(QWidget, f"box_{row}_{col}")
if box and hasattr(box, "circle"):
box.circle.setPixmap(box.circle.pixmaps[status])
box.circle.status = status
# ========== 对外接口:获取选中的设备名 ==========
def get_selected_device(self, row, col):
"""获取指定行列的选中设备名"""
box = self.findChild(QWidget, f"box_{row}_{col}")
if box and hasattr(box, "dropdown"):
return box.dropdown.get_selected_device()
return None
# ========== 对外接口:获取毫秒值 ==========
def get_ms_value(self, row, col):
"""获取指定行列的毫秒值如“5ms”"""
box = self.findChild(QWidget, f"box_{row}_{col}")
if box and hasattr(box, "ms_edit"):
# return box.ms_edit.text()
text = box.ms_edit.text().strip()
# 用正则提取数字(支持整数/小数,如"5"、"3.8"、"10.2ms"
import re
number_match = re.search(r"(\d+(?:\.\d+)?)", text)
if number_match:
return number_match.group(1)
return None
if __name__ == "__main__":
app = QApplication(sys.argv)
dialog = SystemDiagnosticsDialog()
dialog.show()
# 1. 设置0行0列的状态为“警告”状态
dialog.set_circle_status(0, 0, "warning")
# 2. 获取1行2列的选中设备名
device = dialog.get_selected_device(1, 2)
print(f"选中设备:{device}")
# 3. 获取3行1列的毫秒值
ms = dialog.get_ms_value(3, 1)
print(f"毫秒值:{ms}")
sys.exit(app.exec())