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 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.scaled( 12, 9, Qt.KeepAspectRatio, Qt.SmoothTransformation ) ) 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())