From d074ac4e1613a2c6804a5c0ca3cb3911a3d169d3 Mon Sep 17 00:00:00 2001 From: "COT001\\DEV" <871066422@qq.com> Date: Wed, 1 Apr 2026 13:47:26 +0800 Subject: [PATCH] xiufubug --- PCM_Viewer.spec | 15 +++++--- build.py | 13 ++++++- main.py | 96 +++++++++++++++++++++++++++++++++++++------------ 3 files changed, 96 insertions(+), 28 deletions(-) diff --git a/PCM_Viewer.spec b/PCM_Viewer.spec index 40303f5..abc6b66 100644 --- a/PCM_Viewer.spec +++ b/PCM_Viewer.spec @@ -26,20 +26,27 @@ 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, target_arch=None, codesign_identity=None, entitlements_file=None, + contents_directory='PCM_Viewer_lib', +) +coll = COLLECT( + exe, + a.binaries, + a.datas, + strip=False, + upx=False, + upx_exclude=[], + name='PCM_Viewer', ) diff --git a/build.py b/build.py index b780823..2d49045 100644 --- a/build.py +++ b/build.py @@ -47,6 +47,13 @@ def main(): "--name=PCM_Viewer", "--windowed", # 不显示控制台窗口 "--onefile" if not onedir else "--onedir", + ] + + # onedir模式:将依赖文件放到专用子文件夹,避免与其他程序冲突 + if onedir: + cmd.append("--contents-directory=PCM_Viewer_lib") + + cmd.extend([ # ========== 启动速度优化 ========== "--noupx", # 不使用 UPX 压缩(UPX 会增加启动时间) "--optimize=2", # Python 字节码优化级别(0-2,2 最高) @@ -67,7 +74,7 @@ def main(): # 只收集必要的 PyQt6 模块(不收集全部,减少体积) "--collect-all=PyQt6.QtWebEngineWidgets", # WebEngine 需要完整收集 "main.py" - ] + ]) print("开始打包...") print(f"模式: {'单文件' if not onedir else '文件夹'}") @@ -87,6 +94,10 @@ def main(): else: print("文件夹位置: dist/PCM_Viewer/") print("可执行文件: dist/PCM_Viewer/PCM_Viewer.exe") + print("\n注意:") + print("1. 所有依赖文件已放入 PCM_Viewer_lib 子文件夹,避免与其他程序冲突") + print("2. 可以将 PCM_Viewer.exe 和 PCM_Viewer_lib 文件夹一起复制到任意位置") + print("3. 可以将多个不同的 onedir 程序放在同一目录") else: print("\n打包失败!") sys.exit(1) diff --git a/main.py b/main.py index f26f7f1..3b5c162 100644 --- a/main.py +++ b/main.py @@ -1472,6 +1472,21 @@ class UDPCommandListener(QObject): self.port = port self.running = False self.socket = None + + @staticmethod + def is_port_available(port: int) -> bool: + """检测端口是否可用""" + test_socket = None + try: + test_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + test_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + test_socket.bind(('127.0.0.1', port)) + return True + except OSError: + return False + finally: + if test_socket: + test_socket.close() def start(self): """在后台线程启动 UDP 监听""" @@ -1485,7 +1500,7 @@ class UDPCommandListener(QObject): 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) + self.socket.settimeout(0.1) # 减少超时时间,加快关闭响应 print(f"[UDP] 监听端口 {self.port}...") @@ -1526,7 +1541,6 @@ class UDPCommandListener(QObject): 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() @@ -1563,13 +1577,14 @@ class UDPCommandListener(QObject): # ----------------------------- class MainWindow(QMainWindow): - def __init__(self, start_hidden=False): + def __init__(self, start_hidden=False, enable_edit=False): super().__init__() self.setWindowTitle("PCM Viewer - Widgets Dashboard") self.resize(1400, 840) # 设置焦点策略,确保能够接收键盘事件(特别是 ESC 键) self.setFocusPolicy(Qt.FocusPolicy.StrongFocus) - self._edit_mode = True + self._enable_edit = enable_edit # 是否启用编辑模式 + self._edit_mode = enable_edit # 默认根据enable_edit决定 # 全屏编辑标志(展示模式本身也会全屏) self._fullscreen_edit = False self._first_show = True # 用于在第一次显示时最大化并校正滚动条 @@ -1738,6 +1753,9 @@ class MainWindow(QMainWindow): if want_full: # 真全屏:使用 Qt 的 showFullScreen,退出时用 showMaximized 恢复普通 Windows 窗口 self.showFullScreen() + # 全屏后强制窗口到最上层 + self.raise_() + self.activateWindow() # 全屏后确保窗口获得焦点,以便接收键盘事件(ESC 键) self.setFocus() # 全屏时关闭滚动条,避免 1920x1080 屏幕上还出现滚动条 @@ -1811,7 +1829,7 @@ class MainWindow(QMainWindow): self._apply_ui_mode() def _apply_ui_mode(self): - """根据当前模式控制按钮可用性,保证“全屏编辑只拖拽/缩放”更纯粹。""" + """根据当前模式控制按钮可用性,保证"全屏编辑只拖拽/缩放"更纯粹。""" if not self._edit_mode: # 展示模式:禁用编辑相关,并隐藏右侧/工具栏,只保留全屏画布 for b in ( @@ -1827,10 +1845,12 @@ class MainWindow(QMainWindow): self.btn_load, ): b.setEnabled(False) - self.btn_mode.setEnabled(True) # 允许退出展示 - # 右侧和工具栏隐藏,达到“画布全屏”效果(Esc 或按钮退出) + # 如果启用了编辑模式,允许切换回编辑;否则隐藏按钮 + self.btn_mode.setEnabled(self._enable_edit) + self.btn_mode.setVisible(self._enable_edit) + # 右侧和工具栏隐藏,达到"画布全屏"效果(Esc 或按钮退出) self.right_panel.setVisible(False) - self.toolbar_widget.setVisible(False) + self.toolbar_widget.setVisible(self._enable_edit) # 全屏展示:去掉 splitter 句柄、关闭滚动条,避免白边/滚动条露出 self.splitter.setHandleWidth(0) self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) @@ -2079,9 +2099,13 @@ class MainWindow(QMainWindow): def keyPressEvent(self, event): """ESC 支持退出全屏展示 / 全屏编辑""" if event.key() == Qt.Key.Key_Escape: - # 如果当前是展示模式:ESC 直接切回编辑模式(并退出全屏) + # 如果当前是展示模式 if not self._edit_mode: - self._toggle_mode() + # 如果启用了编辑模式,ESC切回编辑;否则隐藏窗口 + if self._enable_edit: + self._toggle_mode() + else: + self.hide() return # 如果是全屏编辑:只退出全屏编辑,保留编辑模式 if self._fullscreen_edit: @@ -2169,6 +2193,7 @@ class MainWindow(QMainWindow): self.view.load_layout(path) self._layout_path = path self._update_current_file_label() + self._refresh_item_list() # 更新组件列表 def _setup_tray_icon(self): """设置系统托盘图标""" @@ -2215,6 +2240,9 @@ class MainWindow(QMainWindow): def _show_window(self): """显示窗口""" + # 先恢复窗口可见性,避免从最小化直接全屏导致显示不全 + if self.isMinimized(): + self.setWindowState(Qt.WindowState.WindowNoState) self.show() self.activateWindow() self.raise_() @@ -2224,12 +2252,21 @@ class MainWindow(QMainWindow): if layout_path: self.load_layout(layout_path) self._show_window() - # 进入展示模式 + # 确保进入展示模式(会自动处理全屏和label边框等) if self._edit_mode: self._toggle_mode() - # 窗口全屏 - self.showFullScreen() - + else: + # 已经是展示模式,确保所有组件锁定且边框隐藏 + for it in self.view.scene_obj.items(): + if isinstance(it, DashboardItem): + it.setFlag(QGraphicsRectItem.GraphicsItemFlag.ItemIsMovable, False) + it.setFlag(QGraphicsRectItem.GraphicsItemFlag.ItemIsSelectable, False) + if isinstance(it, (LabelItem, ArrowItem)): + it.set_edit_mode(False) + self.view.set_edit_mode(False) + self._update_window_fullscreen() + self._apply_ui_mode() + def closeEvent(self, event): """关闭事件:最小化到托盘而不是退出""" if self.tray_icon and self.tray_icon.isVisible(): @@ -2242,6 +2279,9 @@ class MainWindow(QMainWindow): 2000 ) else: + # 真正退出时,确保清理资源 + if hasattr(self, '_udp_listener'): + self._udp_listener.stop() event.accept() @@ -2254,10 +2294,17 @@ def main(): 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('--show', '-s', action='store_true', help='启动时显示窗口(默认隐藏)') + parser.add_argument('--edit', '-e', action='store_true', help='启用编辑模式(默认仅支持展示)') parser.add_argument('--udp-port', '-p', type=int, default=9876, help='UDP监听端口(默认9876)') args = parser.parse_args() + # 检测 UDP 端口是否可用 + if not UDPCommandListener.is_port_available(args.udp_port): + print(f"[错误] UDP 端口 {args.udp_port} 已被占用,程序可能已在运行") + print(f"[错误] 请检查是否有其他实例正在运行,或使用 --udp-port 指定其他端口") + sys.exit(1) + # QWebEngineView 必须在 QApplication 创建之前导入 from PyQt6.QtWebEngineWidgets import QWebEngineView @@ -2267,9 +2314,9 @@ def main(): # 应用全局样式 apply_styles(app) - # 创建主窗口(根据参数决定是否启动时隐藏) - start_hidden = args.hidden or args.fullscreen # 全屏模式也先隐藏,等待UDP命令 - win = MainWindow(start_hidden=start_hidden) + # 创建主窗口(默认隐藏,除非指定 --show 或 --edit) + start_hidden = not (args.show or args.edit) # --edit 或 --show 时显示窗口 + win = MainWindow(start_hidden=start_hidden, enable_edit=args.edit) # 如果提供了布局文件路径,加载它 if args.layout and os.path.exists(args.layout): @@ -2284,16 +2331,19 @@ def main(): udp_listener.exit_signal.connect(app.quit) udp_listener.start() + # 保存到窗口对象,以便在关闭时清理 + win._udp_listener = udp_listener + # 根据参数决定是否显示窗口 if args.fullscreen: # 全屏模式:显示并进入展示模式 win._show_and_fullscreen() - elif args.hidden: - # 隐藏模式:不显示窗口,仅托盘图标 - print("[Main] 启动时隐藏窗口,等待UDP命令...") - else: - # 正常模式:显示窗口 + elif args.show: + # 显示模式:显示窗口 win.show() + else: + # 默认隐藏模式:不显示窗口,仅托盘图标 + print("[Main] 启动时隐藏窗口,等待UDP命令...") # 应用程序退出时清理 def on_quit():