- 重新启用run.io_bound()调用,使用后台线程执行PowerShell脚本 - 为所有文件保存操作添加按钮防重复点击功能 - 新增win32_helper模块,提供Win32 API和COM接口的文件对话框 - 简化导出最佳方案DXF的代码结构 - 改进异步操作和错误处理机制
233 lines
8.7 KiB
Python
233 lines
8.7 KiB
Python
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
|