子进程效率低,换成udp接收控制指令

main
risingLee 2026-02-26 16:40:24 +08:00
parent 73fd35edb8
commit d181dd4da8
8 changed files with 251 additions and 17 deletions

View File

@ -115,3 +115,4 @@ A: 确保打包时包含了 `PyQt6.QtWebEngineWidgets`,打包脚本已自动

View File

@ -26,13 +26,16 @@ pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[('O', None, 'OPTION'), ('O', None, 'OPTION')],
exclude_binaries=True,
name='PCM_Viewer',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=False,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
@ -40,12 +43,3 @@ exe = EXE(
codesign_identity=None,
entitlements_file=None,
)
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=False,
upx_exclude=[],
name='PCM_Viewer',
)

View File

@ -117,3 +117,4 @@ Measure-Command { Start-Process -FilePath "dist\PCM_Viewer.exe" -Wait }

View File

@ -60,3 +60,4 @@ pip install -r requirements.txt

View File

@ -29,3 +29,4 @@ pause

View File

@ -27,3 +27,4 @@ python build.py

View File

@ -96,3 +96,4 @@ if __name__ == "__main__":

246
main.py
View File

@ -12,6 +12,8 @@ from __future__ import annotations
import json
import os
import sys
import socket
import threading
from dataclasses import dataclass, asdict, field
from typing import Optional, Dict, Any
import math
@ -24,10 +26,12 @@ os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "0"
from PyQt6.QtCore import Qt, QRectF, pyqtSignal, QObject, QTimer, QPointF, QSizeF
from PyQt6.QtWidgets import QSizePolicy
from PyQt6.QtGui import QFontMetrics
from PyQt6.QtGui import QBrush, QColor, QPen, QPixmap, QFont, QCursor, QShortcut, QKeySequence
from PyQt6.QtGui import QBrush, QColor, QPen, QPixmap, QFont, QCursor, QShortcut, QKeySequence, QIcon
from PyQt6.QtWidgets import (
QApplication,
QMainWindow,
QSystemTrayIcon,
QMenu,
QWidget,
QHBoxLayout,
QVBoxLayout,
@ -48,6 +52,7 @@ from PyQt6.QtWidgets import (
QMessageBox,
QPlainTextEdit,
QListWidget,
QAction,
QListWidgetItem,
QColorDialog,
QGroupBox,
@ -1451,12 +1456,115 @@ class InfluxConfigDialog(QDialog):
pass
# -----------------------------
# UDP 命令监听器
# -----------------------------
class UDPCommandListener(QObject):
"""UDP 命令监听器,接收来自 PCM_Report 的命令"""
show_signal = pyqtSignal()
hide_signal = pyqtSignal()
load_layout_signal = pyqtSignal(str)
fullscreen_signal = pyqtSignal()
exit_signal = pyqtSignal()
def __init__(self, port=9876):
super().__init__()
self.port = port
self.running = False
self.socket = None
def start(self):
"""在后台线程启动 UDP 监听"""
self.running = True
thread = threading.Thread(target=self._listen, daemon=True)
thread.start()
def _listen(self):
"""UDP 监听循环"""
try:
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.socket.bind(('127.0.0.1', self.port))
self.socket.settimeout(1.0)
print(f"[UDP] 监听端口 {self.port}...")
while self.running:
try:
data, addr = self.socket.recvfrom(4096)
message = data.decode('utf-8')
print(f"[UDP] 收到命令: {message}")
self._handle_command(message)
except socket.timeout:
continue
except Exception as e:
print(f"[UDP] 处理错误: {e}")
except Exception as e:
print(f"[UDP] 启动失败: {e}")
finally:
if self.socket:
self.socket.close()
def _handle_command(self, message: str):
"""处理接收到的命令"""
try:
cmd = json.loads(message)
action = cmd.get('action')
if action == 'show':
self.show_signal.emit()
elif action == 'hide':
self.hide_signal.emit()
elif action == 'load_layout':
path = cmd.get('path', '')
if path:
self.load_layout_signal.emit(path)
elif action == 'fullscreen':
self.fullscreen_signal.emit()
elif action == 'show_and_fullscreen':
path = cmd.get('path', '')
if path:
self.load_layout_signal.emit(path)
self.show_signal.emit()
self.fullscreen_signal.emit()
elif action == 'exit':
self.exit_signal.emit()
except json.JSONDecodeError:
# 兼容简单文本命令
if message == 'show':
self.show_signal.emit()
elif message == 'hide':
self.hide_signal.emit()
elif message.startswith('load:'):
path = message[5:]
self.load_layout_signal.emit(path)
elif message == 'fullscreen':
self.fullscreen_signal.emit()
elif message == 'exit':
self.exit_signal.emit()
def stop(self):
"""停止监听"""
self.running = False
if self.socket:
self.socket.close()
def send_response(self, addr, message: str):
"""发送响应到指定地址"""
try:
self.socket.sendto(message.encode('utf-8'), addr)
except Exception as e:
print(f"[UDP] 发送响应失败: {e}")
# -----------------------------
# 主窗口
# -----------------------------
class MainWindow(QMainWindow):
def __init__(self):
def __init__(self, start_hidden=False):
super().__init__()
self.setWindowTitle("PCM Viewer - Widgets Dashboard")
self.resize(1400, 840)
@ -1466,6 +1574,7 @@ class MainWindow(QMainWindow):
# 全屏编辑标志(展示模式本身也会全屏)
self._fullscreen_edit = False
self._first_show = True # 用于在第一次显示时最大化并校正滚动条
self._start_hidden = start_hidden # 启动时是否隐藏窗口
self.view = DashboardView()
self.prop = PropertyPanel()
@ -1476,6 +1585,10 @@ class MainWindow(QMainWindow):
# 全局 Influx 客户端:延迟创建,只在需要时初始化(减少启动时间)
self.influx_client = None
# 系统托盘图标
self.tray_icon = None
self._setup_tray_icon()
self.splitter = QSplitter()
# 左侧:画布
self.splitter.addWidget(self.view)
@ -2051,23 +2164,144 @@ class MainWindow(QMainWindow):
# 这里避免频繁弹窗,暂时只打印,也可以根据需要改成状态栏提示
print("Influx error:", msg)
def load_layout(self, path: str):
"""加载布局文件(代理调用 DashboardView 的 load_layout"""
if hasattr(self.view, 'load_layout'):
self.view.load_layout(path)
self._layout_path = path
self._update_current_file_label()
def _setup_tray_icon(self):
"""设置系统托盘图标"""
if not QSystemTrayIcon.isSystemTrayAvailable():
print("[Tray] 系统托盘不可用")
return
self.tray_icon = QSystemTrayIcon(self)
# 使用默认图标(可以替换为自定义图标)
icon = self.style().standardIcon(self.style().StandardPixmap.SP_ComputerIcon)
self.tray_icon.setIcon(icon)
# 创建托盘菜单
tray_menu = QMenu()
show_action = QAction("显示窗口", self)
show_action.triggered.connect(self._show_window)
tray_menu.addAction(show_action)
hide_action = QAction("隐藏窗口", self)
hide_action.triggered.connect(self.hide)
tray_menu.addAction(hide_action)
tray_menu.addSeparator()
quit_action = QAction("退出", self)
quit_action.triggered.connect(QApplication.instance().quit)
tray_menu.addAction(quit_action)
self.tray_icon.setContextMenu(tray_menu)
# 双击托盘图标显示窗口
self.tray_icon.activated.connect(self._on_tray_activated)
# 显示托盘图标
self.tray_icon.show()
self.tray_icon.setToolTip("PCM Viewer")
def _on_tray_activated(self, reason):
"""托盘图标被激活"""
if reason == QSystemTrayIcon.ActivationReason.DoubleClick:
self._show_window()
def _show_window(self):
"""显示窗口"""
self.show()
self.activateWindow()
self.raise_()
def _show_and_fullscreen(self, layout_path: str = None):
"""显示窗口并进入全屏展示模式"""
if layout_path:
self.load_layout(layout_path)
self._show_window()
# 进入展示模式
if self._edit_mode:
self._toggle_mode()
# 窗口全屏
self.showFullScreen()
def closeEvent(self, event):
"""关闭事件:最小化到托盘而不是退出"""
if self.tray_icon and self.tray_icon.isVisible():
event.ignore()
self.hide()
self.tray_icon.showMessage(
"PCM Viewer",
"程序已最小化到系统托盘",
QSystemTrayIcon.MessageIcon.Information,
2000
)
else:
event.accept()
def main():
# 抑制 Windows 上 QWebEngineView 的 DirectComposition 警告
import os
os.environ.setdefault('QT_LOGGING_RULES', 'qt.webenginecontext.debug=false')
# 解析命令行参数
import argparse
parser = argparse.ArgumentParser(description='PCM Viewer - 全屏展示工具')
parser.add_argument('layout', nargs='?', help='布局JSON文件路径')
parser.add_argument('--fullscreen', '-f', action='store_true', help='启动时进入全屏模式')
parser.add_argument('--hidden', '-H', action='store_true', help='启动时隐藏窗口(仅显示托盘图标)')
parser.add_argument('--udp-port', '-p', type=int, default=9876, help='UDP监听端口默认9876')
args = parser.parse_args()
# QWebEngineView 必须在 QApplication 创建之前导入
# 已经在文件顶部导入,这里确保环境变量已设置
from PyQt6.QtWebEngineWidgets import QWebEngineView # 确保已导入
from PyQt6.QtWebEngineWidgets import QWebEngineView
app = QApplication(sys.argv)
app.setQuitOnLastWindowClosed(False) # 关闭窗口时不退出,保持托盘运行
# 应用全局样式
apply_styles(app)
win = MainWindow()
# 创建主窗口(根据参数决定是否启动时隐藏)
start_hidden = args.hidden or args.fullscreen # 全屏模式也先隐藏等待UDP命令
win = MainWindow(start_hidden=start_hidden)
# 如果提供了布局文件路径,加载它
if args.layout and os.path.exists(args.layout):
win.load_layout(args.layout)
# 启动 UDP 监听器
udp_listener = UDPCommandListener(port=args.udp_port)
udp_listener.show_signal.connect(win._show_window)
udp_listener.hide_signal.connect(win.hide)
udp_listener.load_layout_signal.connect(win.load_layout)
udp_listener.fullscreen_signal.connect(lambda: win._show_and_fullscreen())
udp_listener.exit_signal.connect(app.quit)
udp_listener.start()
# 根据参数决定是否显示窗口
if args.fullscreen:
# 全屏模式:显示并进入展示模式
win._show_and_fullscreen()
elif args.hidden:
# 隐藏模式:不显示窗口,仅托盘图标
print("[Main] 启动时隐藏窗口等待UDP命令...")
else:
# 正常模式:显示窗口
win.show()
# 应用程序退出时清理
def on_quit():
udp_listener.stop()
app.aboutToQuit.connect(on_quit)
sys.exit(app.exec())