xiufubug
parent
d470f1f82c
commit
d074ac4e16
|
|
@ -26,20 +26,27 @@ 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,
|
||||||
target_arch=None,
|
target_arch=None,
|
||||||
codesign_identity=None,
|
codesign_identity=None,
|
||||||
entitlements_file=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',
|
||||||
)
|
)
|
||||||
|
|
|
||||||
13
build.py
13
build.py
|
|
@ -47,6 +47,13 @@ def main():
|
||||||
"--name=PCM_Viewer",
|
"--name=PCM_Viewer",
|
||||||
"--windowed", # 不显示控制台窗口
|
"--windowed", # 不显示控制台窗口
|
||||||
"--onefile" if not onedir else "--onedir",
|
"--onefile" if not onedir else "--onedir",
|
||||||
|
]
|
||||||
|
|
||||||
|
# onedir模式:将依赖文件放到专用子文件夹,避免与其他程序冲突
|
||||||
|
if onedir:
|
||||||
|
cmd.append("--contents-directory=PCM_Viewer_lib")
|
||||||
|
|
||||||
|
cmd.extend([
|
||||||
# ========== 启动速度优化 ==========
|
# ========== 启动速度优化 ==========
|
||||||
"--noupx", # 不使用 UPX 压缩(UPX 会增加启动时间)
|
"--noupx", # 不使用 UPX 压缩(UPX 会增加启动时间)
|
||||||
"--optimize=2", # Python 字节码优化级别(0-2,2 最高)
|
"--optimize=2", # Python 字节码优化级别(0-2,2 最高)
|
||||||
|
|
@ -67,7 +74,7 @@ def main():
|
||||||
# 只收集必要的 PyQt6 模块(不收集全部,减少体积)
|
# 只收集必要的 PyQt6 模块(不收集全部,减少体积)
|
||||||
"--collect-all=PyQt6.QtWebEngineWidgets", # WebEngine 需要完整收集
|
"--collect-all=PyQt6.QtWebEngineWidgets", # WebEngine 需要完整收集
|
||||||
"main.py"
|
"main.py"
|
||||||
]
|
])
|
||||||
|
|
||||||
print("开始打包...")
|
print("开始打包...")
|
||||||
print(f"模式: {'单文件' if not onedir else '文件夹'}")
|
print(f"模式: {'单文件' if not onedir else '文件夹'}")
|
||||||
|
|
@ -87,6 +94,10 @@ def main():
|
||||||
else:
|
else:
|
||||||
print("文件夹位置: dist/PCM_Viewer/")
|
print("文件夹位置: dist/PCM_Viewer/")
|
||||||
print("可执行文件: dist/PCM_Viewer/PCM_Viewer.exe")
|
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:
|
else:
|
||||||
print("\n打包失败!")
|
print("\n打包失败!")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
|
||||||
92
main.py
92
main.py
|
|
@ -1473,6 +1473,21 @@ class UDPCommandListener(QObject):
|
||||||
self.running = False
|
self.running = False
|
||||||
self.socket = None
|
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):
|
def start(self):
|
||||||
"""在后台线程启动 UDP 监听"""
|
"""在后台线程启动 UDP 监听"""
|
||||||
self.running = True
|
self.running = True
|
||||||
|
|
@ -1485,7 +1500,7 @@ class UDPCommandListener(QObject):
|
||||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
self.socket.bind(('127.0.0.1', self.port))
|
self.socket.bind(('127.0.0.1', self.port))
|
||||||
self.socket.settimeout(1.0)
|
self.socket.settimeout(0.1) # 减少超时时间,加快关闭响应
|
||||||
|
|
||||||
print(f"[UDP] 监听端口 {self.port}...")
|
print(f"[UDP] 监听端口 {self.port}...")
|
||||||
|
|
||||||
|
|
@ -1526,7 +1541,6 @@ class UDPCommandListener(QObject):
|
||||||
path = cmd.get('path', '')
|
path = cmd.get('path', '')
|
||||||
if path:
|
if path:
|
||||||
self.load_layout_signal.emit(path)
|
self.load_layout_signal.emit(path)
|
||||||
self.show_signal.emit()
|
|
||||||
self.fullscreen_signal.emit()
|
self.fullscreen_signal.emit()
|
||||||
elif action == 'exit':
|
elif action == 'exit':
|
||||||
self.exit_signal.emit()
|
self.exit_signal.emit()
|
||||||
|
|
@ -1563,13 +1577,14 @@ class UDPCommandListener(QObject):
|
||||||
# -----------------------------
|
# -----------------------------
|
||||||
|
|
||||||
class MainWindow(QMainWindow):
|
class MainWindow(QMainWindow):
|
||||||
def __init__(self, start_hidden=False):
|
def __init__(self, start_hidden=False, enable_edit=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)
|
||||||
# 设置焦点策略,确保能够接收键盘事件(特别是 ESC 键)
|
# 设置焦点策略,确保能够接收键盘事件(特别是 ESC 键)
|
||||||
self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
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._fullscreen_edit = False
|
||||||
self._first_show = True # 用于在第一次显示时最大化并校正滚动条
|
self._first_show = True # 用于在第一次显示时最大化并校正滚动条
|
||||||
|
|
@ -1738,6 +1753,9 @@ class MainWindow(QMainWindow):
|
||||||
if want_full:
|
if want_full:
|
||||||
# 真全屏:使用 Qt 的 showFullScreen,退出时用 showMaximized 恢复普通 Windows 窗口
|
# 真全屏:使用 Qt 的 showFullScreen,退出时用 showMaximized 恢复普通 Windows 窗口
|
||||||
self.showFullScreen()
|
self.showFullScreen()
|
||||||
|
# 全屏后强制窗口到最上层
|
||||||
|
self.raise_()
|
||||||
|
self.activateWindow()
|
||||||
# 全屏后确保窗口获得焦点,以便接收键盘事件(ESC 键)
|
# 全屏后确保窗口获得焦点,以便接收键盘事件(ESC 键)
|
||||||
self.setFocus()
|
self.setFocus()
|
||||||
# 全屏时关闭滚动条,避免 1920x1080 屏幕上还出现滚动条
|
# 全屏时关闭滚动条,避免 1920x1080 屏幕上还出现滚动条
|
||||||
|
|
@ -1811,7 +1829,7 @@ class MainWindow(QMainWindow):
|
||||||
self._apply_ui_mode()
|
self._apply_ui_mode()
|
||||||
|
|
||||||
def _apply_ui_mode(self):
|
def _apply_ui_mode(self):
|
||||||
"""根据当前模式控制按钮可用性,保证“全屏编辑只拖拽/缩放”更纯粹。"""
|
"""根据当前模式控制按钮可用性,保证"全屏编辑只拖拽/缩放"更纯粹。"""
|
||||||
if not self._edit_mode:
|
if not self._edit_mode:
|
||||||
# 展示模式:禁用编辑相关,并隐藏右侧/工具栏,只保留全屏画布
|
# 展示模式:禁用编辑相关,并隐藏右侧/工具栏,只保留全屏画布
|
||||||
for b in (
|
for b in (
|
||||||
|
|
@ -1827,10 +1845,12 @@ class MainWindow(QMainWindow):
|
||||||
self.btn_load,
|
self.btn_load,
|
||||||
):
|
):
|
||||||
b.setEnabled(False)
|
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.right_panel.setVisible(False)
|
||||||
self.toolbar_widget.setVisible(False)
|
self.toolbar_widget.setVisible(self._enable_edit)
|
||||||
# 全屏展示:去掉 splitter 句柄、关闭滚动条,避免白边/滚动条露出
|
# 全屏展示:去掉 splitter 句柄、关闭滚动条,避免白边/滚动条露出
|
||||||
self.splitter.setHandleWidth(0)
|
self.splitter.setHandleWidth(0)
|
||||||
self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
|
||||||
|
|
@ -2079,9 +2099,13 @@ class MainWindow(QMainWindow):
|
||||||
def keyPressEvent(self, event):
|
def keyPressEvent(self, event):
|
||||||
"""ESC 支持退出全屏展示 / 全屏编辑"""
|
"""ESC 支持退出全屏展示 / 全屏编辑"""
|
||||||
if event.key() == Qt.Key.Key_Escape:
|
if event.key() == Qt.Key.Key_Escape:
|
||||||
# 如果当前是展示模式:ESC 直接切回编辑模式(并退出全屏)
|
# 如果当前是展示模式
|
||||||
if not self._edit_mode:
|
if not self._edit_mode:
|
||||||
|
# 如果启用了编辑模式,ESC切回编辑;否则隐藏窗口
|
||||||
|
if self._enable_edit:
|
||||||
self._toggle_mode()
|
self._toggle_mode()
|
||||||
|
else:
|
||||||
|
self.hide()
|
||||||
return
|
return
|
||||||
# 如果是全屏编辑:只退出全屏编辑,保留编辑模式
|
# 如果是全屏编辑:只退出全屏编辑,保留编辑模式
|
||||||
if self._fullscreen_edit:
|
if self._fullscreen_edit:
|
||||||
|
|
@ -2169,6 +2193,7 @@ class MainWindow(QMainWindow):
|
||||||
self.view.load_layout(path)
|
self.view.load_layout(path)
|
||||||
self._layout_path = path
|
self._layout_path = path
|
||||||
self._update_current_file_label()
|
self._update_current_file_label()
|
||||||
|
self._refresh_item_list() # 更新组件列表
|
||||||
|
|
||||||
def _setup_tray_icon(self):
|
def _setup_tray_icon(self):
|
||||||
"""设置系统托盘图标"""
|
"""设置系统托盘图标"""
|
||||||
|
|
@ -2215,6 +2240,9 @@ class MainWindow(QMainWindow):
|
||||||
|
|
||||||
def _show_window(self):
|
def _show_window(self):
|
||||||
"""显示窗口"""
|
"""显示窗口"""
|
||||||
|
# 先恢复窗口可见性,避免从最小化直接全屏导致显示不全
|
||||||
|
if self.isMinimized():
|
||||||
|
self.setWindowState(Qt.WindowState.WindowNoState)
|
||||||
self.show()
|
self.show()
|
||||||
self.activateWindow()
|
self.activateWindow()
|
||||||
self.raise_()
|
self.raise_()
|
||||||
|
|
@ -2224,11 +2252,20 @@ class MainWindow(QMainWindow):
|
||||||
if layout_path:
|
if layout_path:
|
||||||
self.load_layout(layout_path)
|
self.load_layout(layout_path)
|
||||||
self._show_window()
|
self._show_window()
|
||||||
# 进入展示模式
|
# 确保进入展示模式(会自动处理全屏和label边框等)
|
||||||
if self._edit_mode:
|
if self._edit_mode:
|
||||||
self._toggle_mode()
|
self._toggle_mode()
|
||||||
# 窗口全屏
|
else:
|
||||||
self.showFullScreen()
|
# 已经是展示模式,确保所有组件锁定且边框隐藏
|
||||||
|
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):
|
def closeEvent(self, event):
|
||||||
"""关闭事件:最小化到托盘而不是退出"""
|
"""关闭事件:最小化到托盘而不是退出"""
|
||||||
|
|
@ -2242,6 +2279,9 @@ class MainWindow(QMainWindow):
|
||||||
2000
|
2000
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
# 真正退出时,确保清理资源
|
||||||
|
if hasattr(self, '_udp_listener'):
|
||||||
|
self._udp_listener.stop()
|
||||||
event.accept()
|
event.accept()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2254,10 +2294,17 @@ def main():
|
||||||
parser = argparse.ArgumentParser(description='PCM Viewer - 全屏展示工具')
|
parser = argparse.ArgumentParser(description='PCM Viewer - 全屏展示工具')
|
||||||
parser.add_argument('layout', nargs='?', help='布局JSON文件路径')
|
parser.add_argument('layout', nargs='?', help='布局JSON文件路径')
|
||||||
parser.add_argument('--fullscreen', '-f', action='store_true', help='启动时进入全屏模式')
|
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)')
|
parser.add_argument('--udp-port', '-p', type=int, default=9876, help='UDP监听端口(默认9876)')
|
||||||
args = parser.parse_args()
|
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 创建之前导入
|
# QWebEngineView 必须在 QApplication 创建之前导入
|
||||||
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||||
|
|
||||||
|
|
@ -2267,9 +2314,9 @@ def main():
|
||||||
# 应用全局样式
|
# 应用全局样式
|
||||||
apply_styles(app)
|
apply_styles(app)
|
||||||
|
|
||||||
# 创建主窗口(根据参数决定是否启动时隐藏)
|
# 创建主窗口(默认隐藏,除非指定 --show 或 --edit)
|
||||||
start_hidden = args.hidden or args.fullscreen # 全屏模式也先隐藏,等待UDP命令
|
start_hidden = not (args.show or args.edit) # --edit 或 --show 时显示窗口
|
||||||
win = MainWindow(start_hidden=start_hidden)
|
win = MainWindow(start_hidden=start_hidden, enable_edit=args.edit)
|
||||||
|
|
||||||
# 如果提供了布局文件路径,加载它
|
# 如果提供了布局文件路径,加载它
|
||||||
if args.layout and os.path.exists(args.layout):
|
if args.layout and os.path.exists(args.layout):
|
||||||
|
|
@ -2284,16 +2331,19 @@ def main():
|
||||||
udp_listener.exit_signal.connect(app.quit)
|
udp_listener.exit_signal.connect(app.quit)
|
||||||
udp_listener.start()
|
udp_listener.start()
|
||||||
|
|
||||||
|
# 保存到窗口对象,以便在关闭时清理
|
||||||
|
win._udp_listener = udp_listener
|
||||||
|
|
||||||
# 根据参数决定是否显示窗口
|
# 根据参数决定是否显示窗口
|
||||||
if args.fullscreen:
|
if args.fullscreen:
|
||||||
# 全屏模式:显示并进入展示模式
|
# 全屏模式:显示并进入展示模式
|
||||||
win._show_and_fullscreen()
|
win._show_and_fullscreen()
|
||||||
elif args.hidden:
|
elif args.show:
|
||||||
# 隐藏模式:不显示窗口,仅托盘图标
|
# 显示模式:显示窗口
|
||||||
print("[Main] 启动时隐藏窗口,等待UDP命令...")
|
|
||||||
else:
|
|
||||||
# 正常模式:显示窗口
|
|
||||||
win.show()
|
win.show()
|
||||||
|
else:
|
||||||
|
# 默认隐藏模式:不显示窗口,仅托盘图标
|
||||||
|
print("[Main] 启动时隐藏窗口,等待UDP命令...")
|
||||||
|
|
||||||
# 应用程序退出时清理
|
# 应用程序退出时清理
|
||||||
def on_quit():
|
def on_quit():
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue