子进程效率低,换成udp接收控制指令
parent
73fd35edb8
commit
d181dd4da8
|
|
@ -26,13 +26,16 @@ pyz = PYZ(a.pure)
|
||||||
exe = EXE(
|
exe = EXE(
|
||||||
pyz,
|
pyz,
|
||||||
a.scripts,
|
a.scripts,
|
||||||
|
a.binaries,
|
||||||
|
a.datas,
|
||||||
[('O', None, 'OPTION'), ('O', None, 'OPTION')],
|
[('O', None, 'OPTION'), ('O', None, 'OPTION')],
|
||||||
exclude_binaries=True,
|
|
||||||
name='PCM_Viewer',
|
name='PCM_Viewer',
|
||||||
debug=False,
|
debug=False,
|
||||||
bootloader_ignore_signals=False,
|
bootloader_ignore_signals=False,
|
||||||
strip=False,
|
strip=False,
|
||||||
upx=False,
|
upx=False,
|
||||||
|
upx_exclude=[],
|
||||||
|
runtime_tmpdir=None,
|
||||||
console=False,
|
console=False,
|
||||||
disable_windowed_traceback=False,
|
disable_windowed_traceback=False,
|
||||||
argv_emulation=False,
|
argv_emulation=False,
|
||||||
|
|
@ -40,12 +43,3 @@ exe = EXE(
|
||||||
codesign_identity=None,
|
codesign_identity=None,
|
||||||
entitlements_file=None,
|
entitlements_file=None,
|
||||||
)
|
)
|
||||||
coll = COLLECT(
|
|
||||||
exe,
|
|
||||||
a.binaries,
|
|
||||||
a.datas,
|
|
||||||
strip=False,
|
|
||||||
upx=False,
|
|
||||||
upx_exclude=[],
|
|
||||||
name='PCM_Viewer',
|
|
||||||
)
|
|
||||||
|
|
|
||||||
|
|
@ -117,3 +117,4 @@ Measure-Command { Start-Process -FilePath "dist\PCM_Viewer.exe" -Wait }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -60,3 +60,4 @@ pip install -r requirements.txt
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -96,3 +96,4 @@ if __name__ == "__main__":
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
248
main.py
248
main.py
|
|
@ -12,6 +12,8 @@ from __future__ import annotations
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import socket
|
||||||
|
import threading
|
||||||
from dataclasses import dataclass, asdict, field
|
from dataclasses import dataclass, asdict, field
|
||||||
from typing import Optional, Dict, Any
|
from typing import Optional, Dict, Any
|
||||||
import math
|
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.QtCore import Qt, QRectF, pyqtSignal, QObject, QTimer, QPointF, QSizeF
|
||||||
from PyQt6.QtWidgets import QSizePolicy
|
from PyQt6.QtWidgets import QSizePolicy
|
||||||
from PyQt6.QtGui import QFontMetrics
|
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 (
|
from PyQt6.QtWidgets import (
|
||||||
QApplication,
|
QApplication,
|
||||||
QMainWindow,
|
QMainWindow,
|
||||||
|
QSystemTrayIcon,
|
||||||
|
QMenu,
|
||||||
QWidget,
|
QWidget,
|
||||||
QHBoxLayout,
|
QHBoxLayout,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
|
|
@ -48,6 +52,7 @@ from PyQt6.QtWidgets import (
|
||||||
QMessageBox,
|
QMessageBox,
|
||||||
QPlainTextEdit,
|
QPlainTextEdit,
|
||||||
QListWidget,
|
QListWidget,
|
||||||
|
QAction,
|
||||||
QListWidgetItem,
|
QListWidgetItem,
|
||||||
QColorDialog,
|
QColorDialog,
|
||||||
QGroupBox,
|
QGroupBox,
|
||||||
|
|
@ -1451,12 +1456,115 @@ class InfluxConfigDialog(QDialog):
|
||||||
pass
|
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):
|
class MainWindow(QMainWindow):
|
||||||
def __init__(self):
|
def __init__(self, start_hidden=False):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.setWindowTitle("PCM Viewer - Widgets Dashboard")
|
self.setWindowTitle("PCM Viewer - Widgets Dashboard")
|
||||||
self.resize(1400, 840)
|
self.resize(1400, 840)
|
||||||
|
|
@ -1466,6 +1574,7 @@ class MainWindow(QMainWindow):
|
||||||
# 全屏编辑标志(展示模式本身也会全屏)
|
# 全屏编辑标志(展示模式本身也会全屏)
|
||||||
self._fullscreen_edit = False
|
self._fullscreen_edit = False
|
||||||
self._first_show = True # 用于在第一次显示时最大化并校正滚动条
|
self._first_show = True # 用于在第一次显示时最大化并校正滚动条
|
||||||
|
self._start_hidden = start_hidden # 启动时是否隐藏窗口
|
||||||
|
|
||||||
self.view = DashboardView()
|
self.view = DashboardView()
|
||||||
self.prop = PropertyPanel()
|
self.prop = PropertyPanel()
|
||||||
|
|
@ -1476,6 +1585,10 @@ class MainWindow(QMainWindow):
|
||||||
# 全局 Influx 客户端:延迟创建,只在需要时初始化(减少启动时间)
|
# 全局 Influx 客户端:延迟创建,只在需要时初始化(减少启动时间)
|
||||||
self.influx_client = None
|
self.influx_client = None
|
||||||
|
|
||||||
|
# 系统托盘图标
|
||||||
|
self.tray_icon = None
|
||||||
|
self._setup_tray_icon()
|
||||||
|
|
||||||
self.splitter = QSplitter()
|
self.splitter = QSplitter()
|
||||||
# 左侧:画布
|
# 左侧:画布
|
||||||
self.splitter.addWidget(self.view)
|
self.splitter.addWidget(self.view)
|
||||||
|
|
@ -2051,23 +2164,144 @@ class MainWindow(QMainWindow):
|
||||||
# 这里避免频繁弹窗,暂时只打印,也可以根据需要改成状态栏提示
|
# 这里避免频繁弹窗,暂时只打印,也可以根据需要改成状态栏提示
|
||||||
print("Influx error:", msg)
|
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():
|
def main():
|
||||||
# 抑制 Windows 上 QWebEngineView 的 DirectComposition 警告
|
# 抑制 Windows 上 QWebEngineView 的 DirectComposition 警告
|
||||||
import os
|
|
||||||
os.environ.setdefault('QT_LOGGING_RULES', 'qt.webenginecontext.debug=false')
|
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 创建之前导入
|
# QWebEngineView 必须在 QApplication 创建之前导入
|
||||||
# 已经在文件顶部导入,这里确保环境变量已设置
|
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||||
from PyQt6.QtWebEngineWidgets import QWebEngineView # 确保已导入
|
|
||||||
|
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
|
app.setQuitOnLastWindowClosed(False) # 关闭窗口时不退出,保持托盘运行
|
||||||
|
|
||||||
# 应用全局样式
|
# 应用全局样式
|
||||||
apply_styles(app)
|
apply_styles(app)
|
||||||
|
|
||||||
win = MainWindow()
|
# 创建主窗口(根据参数决定是否启动时隐藏)
|
||||||
win.show()
|
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())
|
sys.exit(app.exec())
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue