diff --git a/gui.py b/gui.py index 6a3dac9..c9f3fea 100644 --- a/gui.py +++ b/gui.py @@ -4,6 +4,9 @@ import os import sys import tempfile +import matplotlib + +matplotlib.use("Agg") import matplotlib.backends.backend_svg import matplotlib.pyplot as plt import pandas as pd @@ -354,133 +357,77 @@ def index(): :param callback: 接收文件路径并执行保存操作的函数 (filepath) -> None :param file_filter: 格式如 "Excel Files (*.xlsx)" """ - # 检测是否为原生模式 (PyWebview) - is_native = False - native_window = None - try: - # 使用 getattr 安全获取 app.native,避免属性不存在错误 - # 并在 reload=True 时 native 可能未能正确初始化 - n_obj = getattr(app, "native", None) - if n_obj and getattr(n_obj, "main_window", None): - is_native = True - native_window = n_obj.main_window - except Exception as e: - print(f"DEBUG: Native check error: {e}") + # 方案:使用 PowerShell 弹出原生保存对话框 (仅限 Windows) + # 这完全避免了 Python UI 库 (Tkinter/PyWebview) 的线程冲突和 PicklingError + import platform + import subprocess - print( - f"DEBUG: save_file_with_dialog called. is_native={is_native}, filename={filename}" - ) - - if is_native and native_window: + if platform.system() == "Windows": try: - # PyWebview 的 create_file_dialog 的 file_types 参数期望一个字符串元组 - # 格式如: ('Description (*.ext)', 'All files (*.*)') - file_types = (file_filter,) + # 构建 PowerShell 脚本 + # 注意:过滤器格式为 "描述|*.ext|所有文件|*.*" + ps_filter = file_filter.replace("(", "|").replace(")", "") + if "|" not in ps_filter: + ps_filter += f"|{os.path.splitext(filename)[1] or '*.*'}" + + # 简单清洗 filter 字符串以适应 PowerShell (e.g., "Excel Files *.xlsx" -> "Excel Files|*.xlsx") + # 这里做一个简化的映射,确保格式正确 + if "Excel" in file_filter: + ps_filter = "Excel Files (*.xlsx)|*.xlsx|All Files (*.*)|*.*" + elif "DXF" in file_filter: + ps_filter = "DXF Files (*.dxf)|*.dxf|All Files (*.*)|*.*" + elif "ZIP" in file_filter: + ps_filter = "ZIP Archives (*.zip)|*.zip|All Files (*.*)|*.*" + else: + ps_filter = "All Files (*.*)|*.*" - print(f"DEBUG: calling create_file_dialog with types={file_types}") + ps_script = f""" + Add-Type -AssemblyName System.Windows.Forms + $d = New-Object System.Windows.Forms.SaveFileDialog + $d.Filter = "{ps_filter}" + $d.FileName = "{filename}" + $d.Title = "保存文件" + if ($d.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {{ + Write-Output $d.FileName + }} + """ - # 在 Native 模式下,create_file_dialog 是同步阻塞的 - # 注意:必须使用 app.native.SAVE_DIALOG - save_path = native_window.create_file_dialog( - app.native.SAVE_DIALOG, - directory="", - save_filename=filename, - file_types=file_types, - ) + # 运行 PowerShell + # 使用 startupinfo 隐藏控制台窗口 (防止黑框闪烁) + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + + print("DEBUG: invoking PowerShell SaveFileDialog...") + + # 使用 run.io_bound 在线程中执行,避免阻塞 UI + def run_ps(): + result = subprocess.run( + ["powershell", "-Command", ps_script], + capture_output=True, + text=True, + startupinfo=startupinfo + ) + return result.stdout.strip() - print(f"DEBUG: save_path result: {save_path}") + from nicegui import run + save_path = await run.io_bound(run_ps) - # 用户取消 - if not save_path: + if save_path: + print(f"DEBUG: PowerShell returned path: {save_path}") + await callback(save_path) + ui.notify(f"文件已保存至: {save_path}", type="positive") return - - # 处理返回类型 (PyWebview 可能返回字符串或列表) - if isinstance(save_path, (list, tuple)): - if not save_path: - return - save_path = save_path[0] - - # 确保文件名后缀正确 - if not save_path.lower().endswith( - os.path.splitext(filename)[1].lower() - ): - save_path += os.path.splitext(filename)[1] - - await callback(save_path) - ui.notify(f"文件已保存至: {save_path}", type="positive") - return # 成功处理,退出 + else: + print("DEBUG: PowerShell dialog cancelled or empty result.") + # 用户取消,直接返回,不回退 + return except Exception as e: - import traceback + print(f"PowerShell dialog failed: {e}") + # 出错则回退到 ui.download - traceback.print_exc() - print(f"ERROR in save_file_with_dialog (native): {e}") - # ui.notify(f"原生保存失败,尝试其他方式: {e}", type="warning") - print(f"原生保存失败,尝试其他方式: {e}") - # 继续向下执行,尝试 fallback - - # 非 Native 模式 (或 Native 失败),尝试使用 Tkinter (仅限本地环境) - try: - import tkinter as tk - from tkinter import filedialog - - from nicegui import run - - print("DEBUG: Attempting Tkinter dialog...") - - def get_save_path_tk(default_name, f_filter): - try: - # 创建隐藏的根窗口 - root = tk.Tk() - root.withdraw() - root.attributes("-topmost", True) # 尝试置顶 - - # 转换 filter 格式: "Excel Files (*.xlsx)" -> [("Excel Files", "*.xlsx")] - filetypes = [] - if "(" in f_filter and ")" in f_filter: - desc = f_filter.split("(")[0].strip() - ext = f_filter.split("(")[1].split(")")[0] - filetypes.append((desc, ext)) - filetypes.append(("All files", "*.*")) - - path = filedialog.asksaveasfilename( - initialfile=default_name, filetypes=filetypes, title="保存文件" - ) - root.destroy() - return path - except Exception as ex: - print(f"Tkinter inner error: {ex}") - return None - - # 在线程中运行 tkinter,避免阻塞 asyncio 事件循环 - save_path = await run.io_bound(get_save_path_tk, filename, file_filter) - - if save_path: - print(f"DEBUG: Tkinter save_path: {save_path}") - # 确保文件名后缀正确 - if not save_path.lower().endswith( - os.path.splitext(filename)[1].lower() - ): - save_path += os.path.splitext(filename)[1] - - await callback(save_path) - ui.notify(f"文件已保存至: {save_path}", type="positive") - return # 成功处理 - elif save_path is None: - print("DEBUG: Tkinter dialog cancelled or failed silently.") - # 如果是用户取消(返回空字符串),通常不需要回退到下载。 - # 但这里如果 Tkinter 彻底失败返回 None,可能需要回退。 - # askopenfilename 返回空字符串表示取消。我们假设 None 是异常。 - # 这里简化处理:只要没拿到路径且没报错,就认为是取消。 - if save_path == "": - return - - except Exception as e: - print(f"Tkinter dialog failed: {e}") - # Fallback to ui.download if tkinter fails - - # 最后的回退方案:浏览器下载 - print("DEBUG: Falling back to ui.download") + # 统一回退方案:浏览器下载 + print("DEBUG: Using ui.download fallback") temp_path = os.path.join(state["temp_dir"], filename) await callback(temp_path) ui.download(temp_path) @@ -1128,18 +1075,18 @@ if getattr(sys, "frozen", False): ) else: # 普通使用环境保留日志功能 - ui.run( - title="海上风电场集电线路优化", - host="127.0.0.1", - reload=True, - port=target_port, - native=False, - ) # ui.run( # title="海上风电场集电线路优化", # host="127.0.0.1", - # port=target_port, # reload=True, - # window_size=(1280, 800), - # native=True, + # port=target_port, + # native=False, # ) + ui.run( + title="海上风电场集电线路优化", + host="127.0.0.1", + port=target_port, + reload=True, + window_size=(1280, 800), + native=True, + )