From 4c48332c06c22d4f3502fac34aed0c606aab4a16 Mon Sep 17 00:00:00 2001 From: yanganjie Date: Fri, 7 Nov 2025 15:45:54 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=B3=BB=E7=BB=9F=E8=AF=8A?= =?UTF-8?q?=E6=96=AD=E5=BC=B9=E7=AA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- controller/bottom_control_controller.py | 89 ++++- controller/main_controller.py | 1 + images/系统诊断下拉箭头.png | Bin 0 -> 172 bytes images/系统诊断小框.png | Bin 0 -> 213 bytes images/系统诊断弹出背景.png | Bin 0 -> 6260 bytes images/系统诊断毫秒背景.png | Bin 0 -> 735 bytes images/系统诊断状态红.png | Bin 0 -> 257 bytes images/系统诊断状态绿.png | Bin 0 -> 251 bytes images/系统诊断状态黄.png | Bin 0 -> 265 bytes utils/image_paths.py | 11 +- view/main_window.py | 3 +- view/widgets/system_center_dialog.py | 4 +- view/widgets/system_diagnostics_dialog.py | 385 ++++++++++++++++++++++ 13 files changed, 486 insertions(+), 7 deletions(-) create mode 100644 images/系统诊断下拉箭头.png create mode 100644 images/系统诊断小框.png create mode 100644 images/系统诊断弹出背景.png create mode 100644 images/系统诊断毫秒背景.png create mode 100644 images/系统诊断状态红.png create mode 100644 images/系统诊断状态绿.png create mode 100644 images/系统诊断状态黄.png create mode 100644 view/widgets/system_diagnostics_dialog.py diff --git a/controller/bottom_control_controller.py b/controller/bottom_control_controller.py index 6309dd1..bd1c000 100644 --- a/controller/bottom_control_controller.py +++ b/controller/bottom_control_controller.py @@ -2,6 +2,7 @@ from PySide6.QtCore import Qt, QPropertyAnimation, QRect, QParallelAnimationGroup, QEasingCurve from view.widgets.system_center_dialog import SystemCenterDialog from view.widgets.bottom_control_widget import BottomControlWidget +from view.widgets.system_diagnostics_dialog import SystemDiagnosticsDialog """ 控制主界面底部的所有按钮, 包括系统诊断、系统中心等的行为。 @@ -19,11 +20,19 @@ class BottomControlController: self.system_center_dialog.hide() # 初始隐藏 (必须) self._init_system_center_dialog_hide_animations() + # 系统诊断弹窗 + self.system_diagnostics_dialog = SystemDiagnosticsDialog(self.main_window) + self.system_diagnostics_dialog.hide() + self._init_system_diagnostics_dialog_hide_animations() + self._bind_dialog_signals() def _bind_buttons(self): # 底部系统中心按钮 → 触发弹窗显示/隐藏 self.bottom_control_widget.center_btn.clicked.connect(self.toggle_system_center_dialog) + + # 底部系统诊断按钮 → 触发弹窗显示/隐藏 + self.bottom_control_widget.diagnosis_btn.clicked.connect(self.toggle_system_diagnostics_dialog) def _bind_dialog_signals(self): """绑定弹窗按钮的信号""" @@ -53,7 +62,7 @@ class BottomControlController: self.hide_anim_group.finished.connect(self.system_center_dialog.hide) def toggle_system_center_dialog(self): - """切换弹窗的显示/隐藏状态""" + """切换系统中心弹窗的显示/隐藏状态""" if self.system_center_dialog.isVisible(): # 已显示 → 隐藏 # self.system_center_dialog.hide() @@ -94,7 +103,7 @@ class BottomControlController: # 设置弹窗位置 self.system_center_dialog.move(dialog_x, dialog_y) - # ------------------- 业务逻辑方法------------------- + # ------------------- 系统中心弹窗业务逻辑------------------- def handle_sys_setting(self): """系统设置按钮的业务逻辑""" # print("执行系统设置逻辑:如打开系统配置窗口、修改参数等") @@ -108,4 +117,78 @@ class BottomControlController: def handle_user_center(self): """用户中心按钮的业务逻辑""" - # print("执行用户中心逻辑:如切换用户、修改密码等") \ No newline at end of file + # print("执行用户中心逻辑:如切换用户、修改密码等") + + # ------------------- 系统诊断弹窗逻辑------------------- + def _init_system_diagnostics_dialog_hide_animations(self): + """初始化系统诊断弹窗隐藏动画(与显示动画反向:滑出+淡出)""" + # 1. 淡出动画(与显示动画时长一致) + self.dia_hide_opacity_anim = QPropertyAnimation( + self.system_diagnostics_dialog, b"windowOpacity", self.system_diagnostics_dialog + ) + self.dia_hide_opacity_anim.setDuration(300) # 显示动画为400ms + self.dia_hide_opacity_anim.setStartValue(1.0) + self.dia_hide_opacity_anim.setEndValue(0.0) + + # 2. 位置动画(从当前位置滑出到下方100px,与显示动画反向) + self.dia_hide_pos_anim = QPropertyAnimation( + self.system_diagnostics_dialog, b"geometry", self.system_diagnostics_dialog + ) + self.dia_hide_pos_anim.setDuration(300) + self.dia_hide_pos_anim.setEasingCurve(QEasingCurve.InQuart) # 滑出曲线与显示反向 + + # 3. 组合动画(同时执行滑出和淡出) + self.dia_hide_anim_group = QParallelAnimationGroup(self.system_diagnostics_dialog) + self.dia_hide_anim_group.addAnimation(self.dia_hide_opacity_anim) + self.dia_hide_anim_group.addAnimation(self.dia_hide_pos_anim) + # 动画结束后强制隐藏弹窗 + self.dia_hide_anim_group.finished.connect(self.system_diagnostics_dialog.hide) + + def toggle_system_diagnostics_dialog(self): + """切换系统诊断弹窗的显示/隐藏状态""" + if self.system_diagnostics_dialog.isVisible(): + # 已显示 → 执行隐藏动画 + self._start_diagnostics_hide_animation() + else: + # 未显示 → 计算位置并显示(触发显示动画) + self._calc_system_diagnostics_dialog_position() + self.system_diagnostics_dialog.show() + + def _calc_system_diagnostics_dialog_position(self): + """计算系统诊断弹窗位置(显示在诊断按钮上方,与中心弹窗布局一致)""" + btn = self.bottom_control_widget.diagnosis_btn # 诊断按钮 + bottom_widget = self.bottom_control_widget + + # 计算按钮在主窗口中的绝对位置 + bottom_pos_rel_main = bottom_widget.pos() # 底部控件相对于主窗口的位置 + btn_pos_rel_bottom = btn.pos() # 诊断按钮相对于底部控件的位置 + btn_pos_rel_main = bottom_pos_rel_main + btn_pos_rel_bottom # 诊断按钮在主窗口中的绝对位置 + + # 计算弹窗坐标(显示在按钮上方,水平居中对齐) + btn_width = btn.width() + dialog_size = self.system_diagnostics_dialog.size() + # 水平方向:与按钮居中对齐 + # dialog_x = btn_pos_rel_main.x() + (btn_width - dialog_size.width()) // 2 + # dialog_x = btn_pos_rel_main.x() + (btn_width - dialog_size.width()) + dialog_x = btn_pos_rel_main.x() # 与系统诊断按钮的左边平齐 + # 垂直方向:在按钮上方(与按钮保持10px间距) + # dialog_y = btn_pos_rel_main.y() - dialog_size.height() - 10 + dialog_y = btn_pos_rel_main.y() - dialog_size.height() + + # 设置弹窗位置(动画会基于此位置执行滑入效果) + self.system_diagnostics_dialog.move(dialog_x, dialog_y) + + def _start_diagnostics_hide_animation(self): + """启动系统诊断弹窗的隐藏动画(滑出+淡出)""" + current_geo = self.system_diagnostics_dialog.geometry() # 当前位置和尺寸 + # 计算隐藏动画终点(当前位置下方100px,与显示动画起点对应) + end_rect = QRect( + current_geo.x(), + current_geo.y() + 100, # 向下滑出100px + current_geo.width(), + current_geo.height() + ) + # 设置动画参数并启动 + self.dia_hide_pos_anim.setStartValue(current_geo) + self.dia_hide_pos_anim.setEndValue(end_rect) + self.dia_hide_anim_group.start() \ No newline at end of file diff --git a/controller/main_controller.py b/controller/main_controller.py index e45aeb9..0b771fa 100644 --- a/controller/main_controller.py +++ b/controller/main_controller.py @@ -18,6 +18,7 @@ class MainController: def showMainWindow(self): self.main_window.showFullScreen() + # self.main_window.show() self.main_window.dispatch_task_widget.set_task_time("task1","15:44 PM") self.main_window.dispatch_task_widget.set_task_time("task2","17:37 PM") self.main_window.segment_task_widget.set_task_time("task1","15:38 PM") diff --git a/images/系统诊断下拉箭头.png b/images/系统诊断下拉箭头.png new file mode 100644 index 0000000000000000000000000000000000000000..4322476fa8abbe720214f871b2d3d9f339ef7cb7 GIT binary patch literal 172 zcmeAS@N?(olHy`uVBq!ia0vp^JV4CJ!3HGRcAO0XQpKJwjv*3LdnYtfy0k-?BHMFWK!WXFa#3>DgaL9oBpa6?QzDA;iUX=HefwJ9F2| ViRP}jngz6;!PC{xWt~$(696V>J<_KIEGZrd3)E9_ppNi%fVjJ zRcZ+e?gv=TDoty8_$!<>L4NVS_vzO@OW%0&+kSiA?oH=Q&n5bC9a2)%5uzO-$Fbr5 anLSV6v)gZ)R_z3ICxfS}pUXO@geCyl{6mcZ literal 0 HcmV?d00001 diff --git a/images/系统诊断弹出背景.png b/images/系统诊断弹出背景.png new file mode 100644 index 0000000000000000000000000000000000000000..958bf7800ee22132f54e4d4c438fb5fa695bfae2 GIT binary patch literal 6260 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 literal 0 HcmV?d00001 diff --git a/images/系统诊断毫秒背景.png b/images/系统诊断毫秒背景.png new file mode 100644 index 0000000000000000000000000000000000000000..d3dd2956aa6c84272bdd25e3680527338bf767d6 GIT binary patch literal 735 zcmV<50wDc~P)s&fD2mU@kOCz{X>=R6Bdt^d3x~+fE<7= z08Lm{q&*02o4B^HJSkSzBM?2e-H6jLu=&{?o=VcnD75`LZ7oR;@#slRtiG5yF4wLI z0rB*CM5{MAUwS~_eA8MPYmjT&!gAEMAuN!iCc6bsCBFm6z;gT0!H$TP_49TR?FZ8} z;;QNHe}Y8mVB79YbVCT}&Oj7q5TDq1H({6CX%NVn++8*JC`d>=dwte9`bRO{Llnf1 z%qOp%?E{zWJ3pFdKLc1dGQiH!4t^SjX)t6yghq!+?&r`{5a|oeh4MM%I!j~RG zlTT`T2WHRV!>$=ZY4z)H*f^-d(~F<`9S2#V3m__#Y*?&qjN$EjpD66fdbQcqhFfGx z+!)Y>#_PCsrOM`}_gOqf13FrXBLljAg0!%XOwRx~F`|*_F90+p={+(9@EeIC1RZ<@ RR0;q9002ovPDHLkV1f|yQC9!} literal 0 HcmV?d00001 diff --git a/images/系统诊断状态红.png b/images/系统诊断状态红.png new file mode 100644 index 0000000000000000000000000000000000000000..716cef9842103892e4b767ad79953cf682bfc7dc GIT binary patch literal 257 zcmeAS@N?(olHy`uVBq!ia0vp@Ak4uAB#T}@sR2?aJY5_^G$u}+=$OUiDBv3Zcit10 zU4`N*Eq4@mUGCO6BVd|dDA-)Vn*Xpu!qI=hgA?s6cUHV)cP(}PyQnVIL_c=j4+b>{ z`3y$(1m-^sU*jW9?uI{CPvEO5nyvbQbq<%vx}xPDWgAyEq$h}+y}jFgl|09~m*O7Z zCLG?PpgQ~9!te=Cbe~@MRuPx2?z}}@^Xd{8x18234ob@VbXQHB_FrV}1hcr7=U@j_FhwPxelF{r5}E+1hGPK$ literal 0 HcmV?d00001 diff --git a/images/系统诊断状态黄.png b/images/系统诊断状态黄.png new file mode 100644 index 0000000000000000000000000000000000000000..b57e019edd42166a90ba063b95e774c9bb2a8872 GIT binary patch literal 265 zcmV+k0rvihP)>=H@{&#OTF?2MrygfjY^h7(IAMO}2I;ptCy3gkc196!Z}Zy_>le z=C(37{&Cjkmb)I+rW0V=&|wX{>f#oHVp(l`4@_h$&;fp`#czPm8wy+jOvWJmf)~H| P00000NkvXXu0mjf#(iqd literal 0 HcmV?d00001 diff --git a/utils/image_paths.py b/utils/image_paths.py index fa1495d..ffdda75 100644 --- a/utils/image_paths.py +++ b/utils/image_paths.py @@ -92,4 +92,13 @@ class ImagePaths: ROUND_BTN_BG2 = ":/icons/images/圆形按钮背景2.png" # 功能:主界面相关 - MAIN_INTERFACE_BACKGROUND = ":/icons/images/主界面背景.png" \ No newline at end of file + MAIN_INTERFACE_BACKGROUND = ":/icons/images/主界面背景.png" + + # 功能: 系统诊断弹窗 + SYSTEM_DIAGNOSTICS_POPUP_BG = "images/系统诊断弹出背景.png" + SYSTEM_DIAGNOSTICS_BOX = "images/系统诊断小框.png" + SYSTEM_DIAGNOSTICS_STATUS_GREEN = "images/系统诊断状态绿.png" + SYSTEM_DIAGNOSTICS_STATUS_YELLOW = "images/系统诊断状态黄.png" + SYSTEM_DIAGNOSTICS_STATUS_RED = "images/系统诊断状态红.png" + SYSTEM_DIAGNOSTICS_MS_BG = "images/系统诊断毫秒背景.png" + SYSTEM_DIAGNOSTICS_DROPDOWN_ARROW = "images/系统诊断下拉箭头.png" \ No newline at end of file diff --git a/view/main_window.py b/view/main_window.py index 705c577..cb91984 100644 --- a/view/main_window.py +++ b/view/main_window.py @@ -63,7 +63,8 @@ class MainWindow(QWidget): # self.setStyleSheet("background-color: #ffffff;") # #001558 # Qt.FramelessWindowHint - # self.setWindowFlags(Qt.FramelessWindowHint) + # 没有顶部的白色边框 + self.setWindowFlags(Qt.FramelessWindowHint) # 无边框 # 设置主界面背景图片 try: diff --git a/view/widgets/system_center_dialog.py b/view/widgets/system_center_dialog.py index 2e9b3f3..be444c9 100644 --- a/view/widgets/system_center_dialog.py +++ b/view/widgets/system_center_dialog.py @@ -60,10 +60,10 @@ class SystemCenterDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) - self.init_ui() + self._init_ui() self.init_animations() # 初始化动画 - def init_ui(self): + def _init_ui(self): # 弹窗基础设置 self.setWindowTitle("系统中心") self.setWindowFlags(Qt.FramelessWindowHint) # 隐藏默认边框 diff --git a/view/widgets/system_diagnostics_dialog.py b/view/widgets/system_diagnostics_dialog.py new file mode 100644 index 0000000..8c89c48 --- /dev/null +++ b/view/widgets/system_diagnostics_dialog.py @@ -0,0 +1,385 @@ +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())