From ee351733fe0e995a97b658cfef4c54815dabbd32 Mon Sep 17 00:00:00 2001 From: risingLee <871066422@qq.com> Date: Thu, 26 Feb 2026 16:41:03 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B1=95=E7=A4=BA=E6=8C=89=E9=92=AE=20?= =?UTF-8?q?=E6=8E=A8=E9=80=81udp?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dashboard_viewer_button.py | 181 +++++++++++++++++++++++++++++++++++++ ui_main.py | 130 +++++++++++++++++++++++++- 2 files changed, 309 insertions(+), 2 deletions(-) create mode 100644 dashboard_viewer_button.py diff --git a/dashboard_viewer_button.py b/dashboard_viewer_button.py new file mode 100644 index 0000000..e8fa69f --- /dev/null +++ b/dashboard_viewer_button.py @@ -0,0 +1,181 @@ +""" +PCM_Report 看板查看器按钮模块 +使用方法:在 ui_main.py 中导入并调用 add_dashboard_button() +""" + +import subprocess +import sys +import os +from pathlib import Path +from PySide6.QtWidgets import ( + QPushButton, QFileDialog, QMessageBox, QHBoxLayout, QWidget +) + + +def get_viewer_executable(): + """ + 获取看板查看器的可执行文件路径 + 优先查找打包后的 exe,否则使用 Python 源文件 + """ + # 获取当前程序所在目录 + if getattr(sys, 'frozen', False): + # 打包后的环境 + current_dir = Path(sys.executable).parent + else: + # 开发环境 + current_dir = Path(__file__).parent + + # 1. 首先查找打包后的 exe(与当前程序同级目录) + exe_path = current_dir / "PCM_Viewer.exe" + if exe_path.exists(): + return str(exe_path), True # True 表示是 exe + + # 2. 查找开发环境的 Python 源文件 + dev_path = Path(r"F:\PyPro\PCM_Viewer\main.py") + if dev_path.exists(): + return str(dev_path), False # False 表示是 py 文件 + + # 3. 尝试相对路径(假设两个项目在同一目录下) + relative_path = current_dir.parent / "PCM_Viewer" / "main.py" + if relative_path.exists(): + return str(relative_path), False + + return None, False + + +def add_dashboard_button(window, toolbar_layout: QHBoxLayout): + """ + 在工具栏添加"打开看板"按钮 + + Args: + window: MainWindow 实例 (用于日志记录和消息框) + toolbar_layout: 工具栏布局 + """ + # 创建按钮 + open_viewer_btn = QPushButton("打开看板") + open_viewer_btn.setToolTip("打开看板查看器 (全屏模式)") + + # 绑定点击事件 + def on_open_viewer(): + """打开 PCM_Viewer 全屏展示""" + # 选择布局文件 + file_path, _ = QFileDialog.getOpenFileName( + window, + "选择看板布局文件", + "", + "JSON文件 (*.json)" + ) + + if not file_path: + return + + # 检查文件是否存在 + if not Path(file_path).exists(): + QMessageBox.warning(window, "警告", "选择的文件不存在!") + return + + # 获取查看器可执行文件 + viewer_path, is_exe = get_viewer_executable() + + if not viewer_path: + QMessageBox.critical( + window, + "错误", + "未找到看板查看器程序!\n" + "请确保 PCM_Viewer.exe 或 main.py 存在。" + ) + return + + # 启动 PCM_Viewer 子进程 + try: + if is_exe: + # 打包后的 exe,直接运行 + cmd = [viewer_path, file_path, "--fullscreen"] + else: + # Python 源文件,使用 python 解释器运行 + cmd = ["python", viewer_path, file_path, "--fullscreen"] + + # 使用 subprocess.Popen 启动独立进程 + subprocess.Popen( + cmd, + shell=False, # 不使用 shell,更安全 + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + # 设置工作目录为查看器所在目录 + cwd=os.path.dirname(viewer_path) if is_exe else None + ) + + # 记录日志 + if hasattr(window, 'logger'): + window.logger.info(f"已启动看板查看器: {viewer_path} {file_path}") + + except Exception as e: + QMessageBox.critical(window, "错误", f"启动看板失败: {str(e)}") + if hasattr(window, 'logger'): + window.logger.error(f"启动看板失败: {e}") + + open_viewer_btn.clicked.connect(on_open_viewer) + + # 添加到工具栏 + toolbar_layout.addWidget(open_viewer_btn) + + return open_viewer_btn + + +# 兼容直接复制到 ui_main.py 的函数形式 +def open_dashboard_viewer(window): + """ + 打开 PCM_Viewer 全屏展示(可直接复制到 ui_main.py 使用) + """ + # 选择布局文件 + file_path, _ = QFileDialog.getOpenFileName( + window, + "选择看板布局文件", + "", + "JSON文件 (*.json)" + ) + + if not file_path: + return + + # 检查文件是否存在 + if not Path(file_path).exists(): + QMessageBox.warning(window, "警告", "选择的文件不存在!") + return + + # 获取查看器可执行文件 + viewer_path, is_exe = get_viewer_executable() + + if not viewer_path: + QMessageBox.critical( + window, + "错误", + "未找到看板查看器程序!\n" + "请确保 PCM_Viewer.exe 或 main.py 存在。" + ) + return + + # 启动 PCM_Viewer 子进程 + try: + if is_exe: + # 打包后的 exe,直接运行 + cmd = [viewer_path, file_path, "--fullscreen"] + else: + # Python 源文件,使用 python 解释器运行 + cmd = ["python", viewer_path, file_path, "--fullscreen"] + + subprocess.Popen( + cmd, + shell=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + cwd=os.path.dirname(viewer_path) if is_exe else None + ) + + if hasattr(window, 'logger'): + window.logger.info(f"已启动看板查看器: {viewer_path} {file_path}") + + except Exception as e: + QMessageBox.critical(window, "错误", f"启动看板失败: {str(e)}") + if hasattr(window, 'logger'): + window.logger.error(f"启动看板失败: {e}") diff --git a/ui_main.py b/ui_main.py index e266c4d..0f08c81 100644 --- a/ui_main.py +++ b/ui_main.py @@ -1,6 +1,6 @@ from pathlib import Path from typing import Dict, Callable, Optional, List, Tuple - +import subprocess from PySide6.QtCore import QTimer, QThread, QObject, Signal, QUrl, QUrlQuery, QDateTime, Qt from PySide6.QtGui import QDesktopServices, QFont from PySide6.QtWidgets import ( @@ -1219,7 +1219,17 @@ class MainWindow(QMainWindow): self.power_off_btn.setFixedHeight(36) self.power_off_btn.clicked.connect(self._power_off_experiment_table) toolbar_layout.addWidget(self.power_off_btn) - + # 数据展示按钮(放在上电和断电之间) + self.data_display_btn = QPushButton("数据展示") + self.data_display_btn.setStyleSheet( + "QPushButton { font-weight:700; font-size:13px; color:white;" + "padding:8px 20px; border-radius:6px; background-color:#2196f3; }" + "QPushButton:hover { background-color:#1976d2; }" + ) + self.data_display_btn.setCursor(Qt.PointingHandCursor) + self.data_display_btn.setFixedHeight(36) + self.data_display_btn.clicked.connect(self._open_dashboard_viewer) + toolbar_layout.addWidget(self.data_display_btn) # 开始记录按钮(仅Debug模式显示) self.start_exp_btn = QPushButton("开始记录") self.start_exp_btn.setStyleSheet( @@ -5483,6 +5493,122 @@ class MainWindow(QMainWindow): self.statusBar().showMessage(f"❌ 实验台断电异常: {e}", 5000) QMessageBox.critical(self, "错误", f"实验台断电时发生异常: {str(e)}") + def _open_dashboard_viewer(self): + """打开 PCM_Viewer 全屏展示""" + # 选择布局文件 + file_path, _ = QFileDialog.getOpenFileName( + self, + "选择看板布局文件", + "", + "JSON文件 (*.json)" + ) + + if not file_path: + return + + # 检查文件是否存在 + from pathlib import Path + if not Path(file_path).exists(): + QMessageBox.warning(self, "警告", "选择的文件不存在!") + return + + # 启动 PCM_Viewer 子进程(隐藏模式,通过UDP通信) + try: + self._start_pcm_viewer(file_path) + except Exception as e: + QMessageBox.critical(self, "错误", f"启动看板失败: {str(e)}") + self.logger.error(f"启动看板失败: {e}") + + def _start_pcm_viewer(self, layout_path: str, udp_port: int = 9876): + """启动 PCM_Viewer 并通过 UDP 发送显示命令 + + Args: + layout_path: 布局文件路径 + udp_port: UDP 通信端口 + """ + import socket + import json + import subprocess + import time + import os + + viewer_path = r"F:\PyPro\PCM_Viewer\dist\PCM_Viewer.exe" + + # 检查 PCM_Viewer 是否已在运行(通过UDP探测) + viewer_running = self._check_viewer_running(udp_port) + + if not viewer_running: + # 启动 PCM_Viewer(隐藏模式) + self.logger.info("启动 PCM_Viewer...") + subprocess.Popen( + [viewer_path, "--hidden"], + shell=False, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + creationflags=subprocess.CREATE_NO_WINDOW # 不显示控制台窗口 + ) + # 等待 PCM_Viewer 启动 + time.sleep(1.5) + + # 通过 UDP 发送显示命令 + self._send_udp_command({ + 'action': 'show_and_fullscreen', + 'path': layout_path + }, udp_port) + + self.logger.info(f"已发送显示命令: {layout_path}") + + def _check_viewer_running(self, port: int = 9876) -> bool: + """检查 PCM_Viewer 是否已在运行 + + Args: + port: UDP 端口 + + Returns: + bool: 是否运行中 + """ + import socket + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(0.5) + # 发送探测命令 + sock.sendto(b'ping', ('127.0.0.1', port)) + sock.close() + return True + except: + return False + + def _send_udp_command(self, command: dict, port: int = 9876): + """发送 UDP 命令到 PCM_Viewer + + Args: + command: 命令字典 + port: UDP 端口 + """ + import socket + import json + + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + message = json.dumps(command, ensure_ascii=False) + sock.sendto(message.encode('utf-8'), ('127.0.0.1', port)) + sock.close() + except Exception as e: + self.logger.error(f"发送UDP命令失败: {e}") + raise + + def _show_pcm_viewer(self): + """显示 PCM_Viewer 窗口(用于菜单或按钮调用)""" + self._send_udp_command({'action': 'show'}) + + def _hide_pcm_viewer(self): + """隐藏 PCM_Viewer 窗口""" + self._send_udp_command({'action': 'hide'}) + + def _exit_pcm_viewer(self): + """退出 PCM_Viewer""" + self._send_udp_command({'action': 'exit'}) + def _write_modbus_control_register(self, value: int) -> bool: """通过原始Socket直接发送Modbus TCP报文写入控制寄存器1200 完全抛弃pymodbus,使用最底层的socket通信,模拟Modbus Poll的行为