main
COT001\DEV 2026-04-01 13:47:26 +08:00
parent d470f1f82c
commit d074ac4e16
3 changed files with 96 additions and 28 deletions

View File

@ -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',
)

View File

@ -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-22 最高)
@ -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)

92
main.py
View File

@ -1473,6 +1473,21 @@ class UDPCommandListener(QObject):
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 监听"""
self.running = True
@ -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:
# 如果启用了编辑模式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,11 +2252,20 @@ 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):
"""关闭事件:最小化到托盘而不是退出"""
@ -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():