import ctypes import ctypes.wintypes import os def show_save_dialog_win32(): """ 使用 ctypes 直接调用 Windows API (GetSaveFileNameW) 不需要子进程,可以在线程中运行 (run.io_bound) """ try: # 定义 OPENFILENAME 结构体 class OPENFILENAME(ctypes.Structure): _fields_ = [ ("lStructSize", ctypes.wintypes.DWORD), ("hwndOwner", ctypes.wintypes.HWND), ("hInstance", ctypes.wintypes.HINSTANCE), ("lpstrFilter", ctypes.wintypes.LPCWSTR), ("lpstrCustomFilter", ctypes.wintypes.LPWSTR), ("nMaxCustFilter", ctypes.wintypes.DWORD), ("nFilterIndex", ctypes.wintypes.DWORD), ("lpstrFile", ctypes.wintypes.LPWSTR), ("nMaxFile", ctypes.wintypes.DWORD), ("lpstrFileTitle", ctypes.wintypes.LPWSTR), ("nMaxFileTitle", ctypes.wintypes.DWORD), ("lpstrInitialDir", ctypes.wintypes.LPCWSTR), ("lpstrTitle", ctypes.wintypes.LPCWSTR), ("Flags", ctypes.wintypes.DWORD), ("nFileOffset", ctypes.wintypes.WORD), ("nFileExtension", ctypes.wintypes.WORD), ("lpstrDefExt", ctypes.wintypes.LPCWSTR), ("lCustData", ctypes.wintypes.LPARAM), ("lpfnHook", ctypes.wintypes.LPVOID), ("lpTemplateName", ctypes.wintypes.LPCWSTR), # 还有更多字段,但这通常足够了 # ("pvReserved", ctypes.wintypes.LPVOID), # ("dwReserved", ctypes.wintypes.DWORD), # ("FlagsEx", ctypes.wintypes.DWORD), ] # 准备缓冲区 filename_buffer = ctypes.create_unicode_buffer(260) # MAX_PATH # 设置初始文件名 filename_buffer.value = "win32_save.xlsx" # 准备过滤器 (用 \0 分隔) # 格式: "描述\0模式\0描述\0模式\0\0" filter_str = "Excel Files (*.xlsx)\0*.xlsx\0All Files (*.*)\0*.*\0\0" ofn = OPENFILENAME() ofn.lStructSize = ctypes.sizeof(OPENFILENAME) ofn.hwndOwner = 0 # NULL ofn.lpstrFilter = filter_str ofn.lpstrFile = ctypes.cast(filename_buffer, ctypes.wintypes.LPWSTR) ofn.nMaxFile = 260 ofn.lpstrDefExt = "xlsx" ofn.lpstrTitle = "保存文件 (Win32 API)" # OFN_OVERWRITEPROMPT | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR ofn.Flags = 0x00000002 | 0x00000800 | 0x00000008 comdlg32 = ctypes.windll.comdlg32 # 调用 API # GetSaveFileNameW 返回非零值表示成功 if comdlg32.GetSaveFileNameW(ctypes.byref(ofn)): return filename_buffer.value else: return None except Exception as e: print(f"Win32 API Error: {e}") return None def show_save_dialog_com(): """ 使用 COM 接口 IFileSaveDialog (Windows Vista+) 提供更现代化的文件保存对话框,支持更多功能 """ try: import ctypes import ctypes.wintypes import uuid # 定义必要的常量 CLSCTX_INPROC_SERVER = 1 S_OK = 0 FOS_OVERWRITEPROMPT = 0x00000002 FOS_PATHMUSTEXIST = 0x00000800 FOS_NOCHANGEDIR = 0x00000008 SIGDN_FILESYSPATH = 0x80058000 # IFileSaveDialog 的 CLSID 和 IID CLSID_FileSaveDialog = uuid.UUID("{C0B4E2F3-BA21-4773-8DBA-335EC946EB8B}") IID_IFileSaveDialog = uuid.UUID("{84bccd23-5fde-4cdb-aea4-af64b83d78ab}") IID_IShellItem = uuid.UUID("{43826d1e-e718-42ee-bc55-a1e261c37bfe}") # 加载 ole32.dll ole32 = ctypes.windll.ole32 # CoInitialize ole32.CoInitialize(None) # CoCreateInstance p_dialog = ctypes.c_void_p() hr = ole32.CoCreateInstance( ctypes.byref(CLSID_FileSaveDialog), None, CLSCTX_INPROC_SERVER, ctypes.byref(IID_IFileSaveDialog), ctypes.byref(p_dialog) ) if hr != S_OK: print(f"CoCreateInstance failed: {hr}") return None # 定义 IFileSaveDialog 的 vtable 方法 # 我们只需要调用 Show, GetResult, SetOptions, SetFileName, SetDefaultExtension, SetFileTypeIndex # 这些方法在 IFileOpenDialog 基类中定义 # SetOptions class IFileSaveDialogVtbl(ctypes.Structure): _fields_ = [ ("QueryInterface", ctypes.c_void_p), ("AddRef", ctypes.c_void_p), ("Release", ctypes.c_void_p), # IModalWindow ("Show", ctypes.c_void_p), # IFileDialog ("SetFileTypes", ctypes.c_void_p), ("SetFileTypeIndex", ctypes.c_void_p), ("GetFileTypeIndex", ctypes.c_void_p), ("Advise", ctypes.c_void_p), ("Unadvise", ctypes.c_void_p), ("SetOptions", ctypes.CFUNCTYPE(ctypes.c_long, ctypes.c_void_p, ctypes.c_ulong)), ("GetOptions", ctypes.c_void_p), ("SetDefaultFolder", ctypes.c_void_p), ("SetFolder", ctypes.c_void_p), ("GetFolder", ctypes.c_void_p), ("GetCurrentSelection", ctypes.c_void_p), ("SetFileName", ctypes.CFUNCTYPE(ctypes.c_long, ctypes.c_void_p, ctypes.c_wchar_p)), ("GetFileName", ctypes.c_void_p), ("SetTitle", ctypes.c_void_p), ("SetOkButtonLabel", ctypes.c_void_p), ("SetFileNameLabel", ctypes.c_void_p), ("GetResult", ctypes.CFUNCTYPE(ctypes.c_long, ctypes.c_void_p, ctypes.POINTER(ctypes.c_void_p))), ("AddPlace", ctypes.c_void_p), ("SetDefaultExtension", ctypes.CFUNCTYPE(ctypes.c_long, ctypes.c_void_p, ctypes.c_wchar_p)), ("Close", ctypes.c_void_p), ("SetClientGuid", ctypes.c_void_p), ("ClearClientData", ctypes.c_void_p), ("SetFilter", ctypes.c_void_p), ] # 获取 vtable vtable = ctypes.cast(p_dialog, ctypes.POINTER(ctypes.POINTER(IFileSaveDialogVtbl))).contents.contents # 调用 SetOptions hr = vtable.SetOptions(p_dialog, FOS_OVERWRITEPROMPT | FOS_PATHMUSTEXIST | FOS_NOCHANGEDIR) if hr != S_OK: print(f"SetOptions failed: {hr}") return None # 调用 SetFileName hr = vtable.SetFileName(p_dialog, "com_save.xlsx") if hr != S_OK: print(f"SetFileName failed: {hr}") return None # 调用 SetDefaultExtension hr = vtable.SetDefaultExtension(p_dialog, "xlsx") if hr != S_OK: print(f"SetDefaultExtension failed: {hr}") return None # 调用 SetFileTypeIndex hr = vtable.SetFileTypeIndex(p_dialog, 1) if hr != S_OK: print(f"SetFileTypeIndex failed: {hr}") return None # 调用 Show hr = vtable.Show(p_dialog, 0) # 0 表示没有父窗口 if hr != S_OK: # 用户取消 return None # 调用 GetResult p_result = ctypes.c_void_p() hr = vtable.GetResult(p_dialog, ctypes.byref(p_result)) if hr != S_OK: print(f"GetResult failed: {hr}") return None # 定义 IShellItem 接口 class IShellItemVtbl(ctypes.Structure): _fields_ = [ ("QueryInterface", ctypes.c_void_p), ("AddRef", ctypes.c_void_p), ("Release", ctypes.c_void_p), ("BindToHandler", ctypes.c_void_p), ("GetParent", ctypes.c_void_p), ("GetDisplayName", ctypes.CFUNCTYPE(ctypes.c_long, ctypes.c_void_p, ctypes.c_ulong, ctypes.POINTER(ctypes.c_wchar_p))), ("GetAttributes", ctypes.c_void_p), ("Compare", ctypes.c_void_p), ] # 获取 IShellItem 的 vtable result_vtable = ctypes.cast(p_result, ctypes.POINTER(ctypes.POINTER(IShellItemVtbl))).contents.contents # 调用 GetDisplayName p_display_name = ctypes.c_wchar_p() hr = result_vtable.GetDisplayName(p_result, SIGDN_FILESYSPATH, ctypes.byref(p_display_name)) if hr != S_OK: print(f"GetDisplayName failed: {hr}") return None filepath = p_display_name.value # 清理 ole32.CoUninitialize() return filepath except ImportError as e: print(f"COM Error: {e}") return None except Exception as e: print(f"COM Error: {e}") import traceback traceback.print_exc() return None