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 0000000..4322476 Binary files /dev/null and b/images/系统诊断下拉箭头.png differ diff --git a/images/系统诊断小框.png b/images/系统诊断小框.png new file mode 100644 index 0000000..c6f72c3 Binary files /dev/null and b/images/系统诊断小框.png differ diff --git a/images/系统诊断弹出背景.png b/images/系统诊断弹出背景.png new file mode 100644 index 0000000..958bf78 Binary files /dev/null and b/images/系统诊断弹出背景.png differ diff --git a/images/系统诊断毫秒背景.png b/images/系统诊断毫秒背景.png new file mode 100644 index 0000000..d3dd295 Binary files /dev/null and b/images/系统诊断毫秒背景.png differ diff --git a/images/系统诊断状态红.png b/images/系统诊断状态红.png new file mode 100644 index 0000000..716cef9 Binary files /dev/null and b/images/系统诊断状态红.png differ diff --git a/images/系统诊断状态绿.png b/images/系统诊断状态绿.png new file mode 100644 index 0000000..7a4ea2e Binary files /dev/null and b/images/系统诊断状态绿.png differ diff --git a/images/系统诊断状态黄.png b/images/系统诊断状态黄.png new file mode 100644 index 0000000..b57e019 Binary files /dev/null and b/images/系统诊断状态黄.png differ 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())