kotonebot 0.5.0__py3-none-any.whl → 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kotonebot/__init__.py +39 -39
- kotonebot/backend/bot.py +312 -312
- kotonebot/backend/color.py +525 -525
- kotonebot/backend/context/__init__.py +3 -3
- kotonebot/backend/context/context.py +1002 -1002
- kotonebot/backend/context/task_action.py +183 -183
- kotonebot/backend/core.py +86 -129
- kotonebot/backend/debug/entry.py +89 -89
- kotonebot/backend/debug/mock.py +78 -78
- kotonebot/backend/debug/server.py +222 -222
- kotonebot/backend/debug/vars.py +351 -351
- kotonebot/backend/dispatch.py +227 -227
- kotonebot/backend/flow_controller.py +196 -196
- kotonebot/backend/image.py +36 -5
- kotonebot/backend/loop.py +222 -208
- kotonebot/backend/ocr.py +535 -535
- kotonebot/backend/preprocessor.py +103 -103
- kotonebot/client/__init__.py +9 -9
- kotonebot/client/device.py +369 -529
- kotonebot/client/fast_screenshot.py +377 -377
- kotonebot/client/host/__init__.py +43 -43
- kotonebot/client/host/adb_common.py +101 -107
- kotonebot/client/host/custom.py +118 -118
- kotonebot/client/host/leidian_host.py +196 -196
- kotonebot/client/host/mumu12_host.py +353 -353
- kotonebot/client/host/protocol.py +214 -214
- kotonebot/client/host/windows_common.py +58 -58
- kotonebot/client/implements/__init__.py +65 -70
- kotonebot/client/implements/adb.py +89 -89
- kotonebot/client/implements/nemu_ipc/__init__.py +11 -11
- kotonebot/client/implements/nemu_ipc/external_renderer_ipc.py +284 -284
- kotonebot/client/implements/nemu_ipc/nemu_ipc.py +327 -327
- kotonebot/client/implements/remote_windows.py +188 -188
- kotonebot/client/implements/uiautomator2.py +85 -85
- kotonebot/client/implements/windows.py +176 -176
- kotonebot/client/protocol.py +69 -69
- kotonebot/client/registration.py +24 -24
- kotonebot/client/scaler.py +467 -0
- kotonebot/config/base_config.py +96 -96
- kotonebot/config/config.py +61 -0
- kotonebot/config/manager.py +36 -36
- kotonebot/core/__init__.py +13 -0
- kotonebot/core/entities/base.py +182 -0
- kotonebot/core/entities/compound.py +75 -0
- kotonebot/core/entities/ocr.py +117 -0
- kotonebot/core/entities/template_match.py +198 -0
- kotonebot/devtools/__init__.py +42 -0
- kotonebot/devtools/cli/__init__.py +6 -0
- kotonebot/devtools/cli/main.py +53 -0
- kotonebot/{tools → devtools}/mirror.py +354 -354
- kotonebot/devtools/project/project.py +41 -0
- kotonebot/devtools/project/scanner.py +202 -0
- kotonebot/devtools/project/schema.py +99 -0
- kotonebot/devtools/resgen/__init__.py +42 -0
- kotonebot/devtools/resgen/codegen.py +331 -0
- kotonebot/devtools/resgen/core.py +94 -0
- kotonebot/devtools/resgen/parsers.py +360 -0
- kotonebot/devtools/resgen/utils.py +158 -0
- kotonebot/devtools/resgen/validation.py +115 -0
- kotonebot/devtools/web/dist/assets/bootstrap-icons-BOrJxbIo.woff +0 -0
- kotonebot/devtools/web/dist/assets/bootstrap-icons-BtvjY1KL.woff2 +0 -0
- kotonebot/devtools/web/dist/assets/ext-language_tools-CD021WJ2.js +2577 -0
- kotonebot/devtools/web/dist/assets/index-B_m5f2LF.js +2836 -0
- kotonebot/devtools/web/dist/assets/index-BlEDyGGa.css +9 -0
- kotonebot/devtools/web/dist/assets/language-client-C9muzqaq.js +128 -0
- kotonebot/devtools/web/dist/assets/mode-python-CtHp76XS.js +476 -0
- kotonebot/devtools/web/dist/icons/symbol-class.svg +3 -0
- kotonebot/devtools/web/dist/icons/symbol-file.svg +3 -0
- kotonebot/devtools/web/dist/icons/symbol-method.svg +3 -0
- kotonebot/devtools/web/dist/index.html +25 -0
- kotonebot/devtools/web/server/__init__.py +0 -0
- kotonebot/devtools/web/server/rest_api.py +217 -0
- kotonebot/devtools/web/server/server.py +85 -0
- kotonebot/errors.py +76 -76
- kotonebot/interop/win/__init__.py +11 -9
- kotonebot/interop/win/_mouse.py +310 -310
- kotonebot/interop/win/message_box.py +313 -313
- kotonebot/interop/win/reg.py +37 -37
- kotonebot/interop/win/shake_mouse.py +224 -0
- kotonebot/interop/win/shortcut.py +43 -43
- kotonebot/interop/win/task_dialog.py +513 -513
- kotonebot/logging/__init__.py +2 -2
- kotonebot/logging/log.py +17 -17
- kotonebot/primitives/__init__.py +19 -17
- kotonebot/primitives/geometry.py +1067 -862
- kotonebot/primitives/visual.py +143 -63
- kotonebot/ui/file_host/sensio.py +36 -36
- kotonebot/ui/file_host/tmp_send.py +54 -54
- kotonebot/ui/pushkit/__init__.py +3 -3
- kotonebot/ui/pushkit/image_host.py +88 -88
- kotonebot/ui/pushkit/protocol.py +13 -13
- kotonebot/ui/pushkit/wxpusher.py +54 -54
- kotonebot/ui/user.py +148 -148
- kotonebot/util.py +436 -436
- {kotonebot-0.5.0.dist-info → kotonebot-0.6.0.dist-info}/METADATA +84 -82
- kotonebot-0.6.0.dist-info/RECORD +105 -0
- kotonebot-0.6.0.dist-info/entry_points.txt +2 -0
- {kotonebot-0.5.0.dist-info → kotonebot-0.6.0.dist-info}/licenses/LICENSE +673 -673
- kotonebot/client/implements/adb_raw.py +0 -163
- kotonebot-0.5.0.dist-info/RECORD +0 -71
- /kotonebot/{tools → devtools/project}/__init__.py +0 -0
- {kotonebot-0.5.0.dist-info → kotonebot-0.6.0.dist-info}/WHEEL +0 -0
- {kotonebot-0.5.0.dist-info → kotonebot-0.6.0.dist-info}/top_level.txt +0 -0
|
@@ -1,513 +1,513 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Windows Task Dialog interop module.
|
|
3
|
-
|
|
4
|
-
This module provides Windows TaskDialog functionality and is only available on Windows systems.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
import platform
|
|
8
|
-
import warnings
|
|
9
|
-
|
|
10
|
-
from kotonebot.util import is_windows
|
|
11
|
-
|
|
12
|
-
# 检查是否在 Windows 平台上
|
|
13
|
-
if not is_windows():
|
|
14
|
-
_WINDOWS_ONLY_MSG = (
|
|
15
|
-
f"TaskDialog is only available on Windows systems. "
|
|
16
|
-
f"Current system: non-Windows\n"
|
|
17
|
-
"To use Windows TaskDialog features, please run this code on a Windows system."
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
# 提供虚拟类以避免导入错误
|
|
22
|
-
class TaskDialog:
|
|
23
|
-
def __init__(self, *args, **kwargs):
|
|
24
|
-
raise ImportError(_WINDOWS_ONLY_MSG)
|
|
25
|
-
|
|
26
|
-
# 导出所有常量作为 None
|
|
27
|
-
__all__ = [
|
|
28
|
-
"TaskDialog",
|
|
29
|
-
"TDCBF_OK_BUTTON", "TDCBF_YES_BUTTON", "TDCBF_NO_BUTTON", "TDCBF_CANCEL_BUTTON",
|
|
30
|
-
"TDCBF_RETRY_BUTTON", "TDCBF_CLOSE_BUTTON",
|
|
31
|
-
"IDOK", "IDCANCEL", "IDABORT", "IDRETRY", "IDIGNORE", "IDYES", "IDNO", "IDCLOSE",
|
|
32
|
-
"TD_WARNING_ICON", "TD_ERROR_ICON", "TD_INFORMATION_ICON", "TD_SHIELD_ICON"
|
|
33
|
-
]
|
|
34
|
-
|
|
35
|
-
# 设置所有常量为 None 或保留为模块级变量
|
|
36
|
-
TDCBF_OK_BUTTON = TDCBF_YES_BUTTON = TDCBF_NO_BUTTON = TDCBF_CANCEL_BUTTON = None
|
|
37
|
-
TDCBF_RETRY_BUTTON = TDCBF_CLOSE_BUTTON = None
|
|
38
|
-
IDOK = IDCANCEL = IDABORT = IDRETRY = IDIGNORE = IDYES = IDNO = IDCLOSE = None
|
|
39
|
-
TD_WARNING_ICON = TD_ERROR_ICON = TD_INFORMATION_ICON = TD_SHIELD_ICON = None
|
|
40
|
-
|
|
41
|
-
# 阻止模块加载
|
|
42
|
-
raise ImportError(_WINDOWS_ONLY_MSG)
|
|
43
|
-
|
|
44
|
-
# 如果是 Windows,继续正常加载
|
|
45
|
-
import ctypes
|
|
46
|
-
from ctypes import wintypes
|
|
47
|
-
import time
|
|
48
|
-
from typing import List, Tuple, Optional
|
|
49
|
-
from typing import Literal
|
|
50
|
-
|
|
51
|
-
__all__ = [
|
|
52
|
-
"TaskDialog",
|
|
53
|
-
"TDCBF_OK_BUTTON", "TDCBF_YES_BUTTON", "TDCBF_NO_BUTTON", "TDCBF_CANCEL_BUTTON",
|
|
54
|
-
"TDCBF_RETRY_BUTTON", "TDCBF_CLOSE_BUTTON",
|
|
55
|
-
"IDOK", "IDCANCEL", "IDABORT", "IDRETRY", "IDIGNORE", "IDYES", "IDNO", "IDCLOSE",
|
|
56
|
-
"TD_WARNING_ICON", "TD_ERROR_ICON", "TD_INFORMATION_ICON", "TD_SHIELD_ICON"
|
|
57
|
-
]
|
|
58
|
-
|
|
59
|
-
# --- Windows API 常量定义 ---
|
|
60
|
-
|
|
61
|
-
# 常用按钮
|
|
62
|
-
TDCBF_OK_BUTTON = 0x0001
|
|
63
|
-
TDCBF_YES_BUTTON = 0x0002
|
|
64
|
-
TDCBF_NO_BUTTON = 0x0004
|
|
65
|
-
TDCBF_CANCEL_BUTTON = 0x0008
|
|
66
|
-
TDCBF_RETRY_BUTTON = 0x0010
|
|
67
|
-
TDCBF_CLOSE_BUTTON = 0x0020
|
|
68
|
-
|
|
69
|
-
# 对话框返回值
|
|
70
|
-
IDOK = 1
|
|
71
|
-
IDCANCEL = 2
|
|
72
|
-
IDABORT = 3
|
|
73
|
-
IDRETRY = 4
|
|
74
|
-
IDIGNORE = 5
|
|
75
|
-
IDYES = 6
|
|
76
|
-
IDNO = 7
|
|
77
|
-
IDCLOSE = 8
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
# 标准图标 (使用 MAKEINTRESOURCE 宏)
|
|
81
|
-
def MAKEINTRESOURCE(i: int) -> wintypes.LPWSTR:
|
|
82
|
-
return wintypes.LPWSTR(i)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
TD_WARNING_ICON = MAKEINTRESOURCE(65535)
|
|
86
|
-
TD_ERROR_ICON = MAKEINTRESOURCE(65534)
|
|
87
|
-
TD_INFORMATION_ICON = MAKEINTRESOURCE(65533)
|
|
88
|
-
TD_SHIELD_ICON = MAKEINTRESOURCE(65532)
|
|
89
|
-
|
|
90
|
-
# Task Dialog 标志
|
|
91
|
-
TDF_ENABLE_HYPERLINKS = 0x0001
|
|
92
|
-
TDF_USE_HICON_MAIN = 0x0002
|
|
93
|
-
TDF_USE_HICON_FOOTER = 0x0004
|
|
94
|
-
TDF_ALLOW_DIALOG_CANCELLATION = 0x0008
|
|
95
|
-
TDF_USE_COMMAND_LINKS = 0x0010
|
|
96
|
-
TDF_USE_COMMAND_LINKS_NO_ICON = 0x0020
|
|
97
|
-
TDF_EXPAND_FOOTER_AREA = 0x0040
|
|
98
|
-
TDF_EXPANDED_BY_DEFAULT = 0x0080
|
|
99
|
-
TDF_VERIFICATION_FLAG_CHECKED = 0x0100
|
|
100
|
-
TDF_SHOW_PROGRESS_BAR = 0x0200
|
|
101
|
-
TDF_SHOW_MARQUEE_PROGRESS_BAR = 0x0400
|
|
102
|
-
TDF_CALLBACK_TIMER = 0x0800
|
|
103
|
-
TDF_POSITION_RELATIVE_TO_WINDOW = 0x1000
|
|
104
|
-
TDF_RTL_LAYOUT = 0x2000
|
|
105
|
-
TDF_NO_DEFAULT_RADIO_BUTTON = 0x4000
|
|
106
|
-
TDF_CAN_BE_MINIMIZED = 0x8000
|
|
107
|
-
|
|
108
|
-
# Task Dialog 通知
|
|
109
|
-
TDN_CREATED = 0
|
|
110
|
-
TDN_NAVIGATED = 1
|
|
111
|
-
TDN_BUTTON_CLICKED = 2
|
|
112
|
-
TDN_HYPERLINK_CLICKED = 3
|
|
113
|
-
TDN_TIMER = 4
|
|
114
|
-
TDN_DESTROYED = 5
|
|
115
|
-
TDN_RADIO_BUTTON_CLICKED = 6
|
|
116
|
-
TDN_DIALOG_CONSTRUCTED = 7
|
|
117
|
-
TDN_VERIFICATION_CLICKED = 8
|
|
118
|
-
TDN_HELP = 9
|
|
119
|
-
TDN_EXPANDO_BUTTON_CLICKED = 10
|
|
120
|
-
|
|
121
|
-
# Windows 消息
|
|
122
|
-
WM_USER = 0x0400
|
|
123
|
-
TDM_SET_PROGRESS_BAR_POS = WM_USER + 114
|
|
124
|
-
|
|
125
|
-
CommonButtonLiteral = Literal["ok", "yes", "no", "cancel", "retry", "close"]
|
|
126
|
-
IconLiteral = Literal["warning", "error", "information", "shield"]
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
# --- C 结构体定义 (使用 ctypes) ---
|
|
130
|
-
|
|
131
|
-
class TASKDIALOG_BUTTON(ctypes.Structure):
|
|
132
|
-
_pack_ = 1
|
|
133
|
-
_fields_ = [("nButtonID", ctypes.c_int),
|
|
134
|
-
("pszButtonText", wintypes.LPCWSTR)]
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
# 定义回调函数指针原型
|
|
138
|
-
PFTASKDIALOGCALLBACK = ctypes.WINFUNCTYPE(
|
|
139
|
-
ctypes.HRESULT, # 返回值
|
|
140
|
-
wintypes.HWND, # hwnd
|
|
141
|
-
ctypes.c_uint, # msg
|
|
142
|
-
ctypes.c_size_t, # wParam
|
|
143
|
-
ctypes.c_size_t, # lParam
|
|
144
|
-
ctypes.c_ssize_t # lpRefData
|
|
145
|
-
)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
class TASKDIALOGCONFIG(ctypes.Structure):
|
|
149
|
-
_pack_ = 1
|
|
150
|
-
_fields_ = [
|
|
151
|
-
("cbSize", ctypes.c_uint),
|
|
152
|
-
("hwndParent", wintypes.HWND),
|
|
153
|
-
("hInstance", wintypes.HINSTANCE),
|
|
154
|
-
("dwFlags", ctypes.c_uint),
|
|
155
|
-
("dwCommonButtons", ctypes.c_uint),
|
|
156
|
-
("pszWindowTitle", wintypes.LPCWSTR),
|
|
157
|
-
("pszMainIcon", wintypes.LPCWSTR),
|
|
158
|
-
("pszMainInstruction", wintypes.LPCWSTR),
|
|
159
|
-
("pszContent", wintypes.LPCWSTR),
|
|
160
|
-
("cButtons", ctypes.c_uint),
|
|
161
|
-
("pButtons", ctypes.POINTER(TASKDIALOG_BUTTON)),
|
|
162
|
-
("nDefaultButton", ctypes.c_int),
|
|
163
|
-
("cRadioButtons", ctypes.c_uint),
|
|
164
|
-
("pRadioButtons", ctypes.POINTER(TASKDIALOG_BUTTON)),
|
|
165
|
-
("nDefaultRadioButton", ctypes.c_int),
|
|
166
|
-
("pszVerificationText", wintypes.LPCWSTR),
|
|
167
|
-
("pszExpandedInformation", wintypes.LPCWSTR),
|
|
168
|
-
("pszExpandedControlText", wintypes.LPCWSTR),
|
|
169
|
-
("pszCollapsedControlText", wintypes.LPCWSTR),
|
|
170
|
-
("pszFooterIcon", wintypes.LPCWSTR),
|
|
171
|
-
("pszFooter", wintypes.LPCWSTR),
|
|
172
|
-
("pfCallback", PFTASKDIALOGCALLBACK), # 使用定义好的原型
|
|
173
|
-
("lpCallbackData", ctypes.c_ssize_t),
|
|
174
|
-
("cxWidth", ctypes.c_uint)
|
|
175
|
-
]
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
# --- 加载 comctl32.dll 并定义函数原型 ---
|
|
179
|
-
|
|
180
|
-
comctl32 = ctypes.WinDLL('comctl32')
|
|
181
|
-
user32 = ctypes.WinDLL('user32')
|
|
182
|
-
|
|
183
|
-
TaskDialogIndirect = comctl32.TaskDialogIndirect
|
|
184
|
-
TaskDialogIndirect.restype = ctypes.HRESULT
|
|
185
|
-
TaskDialogIndirect.argtypes = [
|
|
186
|
-
ctypes.POINTER(TASKDIALOGCONFIG),
|
|
187
|
-
ctypes.POINTER(ctypes.c_int),
|
|
188
|
-
ctypes.POINTER(ctypes.c_int),
|
|
189
|
-
ctypes.POINTER(wintypes.BOOL)
|
|
190
|
-
]
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
# --- Python 封装类 ---
|
|
194
|
-
|
|
195
|
-
class TaskDialog:
|
|
196
|
-
"""
|
|
197
|
-
一个用于显示 Windows TaskDialog 的 Python 封装类。
|
|
198
|
-
支持自定义按钮、单选按钮、进度条、验证框等。
|
|
199
|
-
"""
|
|
200
|
-
|
|
201
|
-
def __init__(self,
|
|
202
|
-
parent_hwnd: Optional[int] = None,
|
|
203
|
-
title: str = "Task Dialog",
|
|
204
|
-
main_instruction: str = "",
|
|
205
|
-
content: str = "",
|
|
206
|
-
common_buttons: int | List[CommonButtonLiteral] = TDCBF_OK_BUTTON,
|
|
207
|
-
main_icon: Optional[wintypes.LPWSTR | int | IconLiteral] = None,
|
|
208
|
-
footer: str = "",
|
|
209
|
-
custom_buttons: Optional[List[Tuple[int, str]]] = None,
|
|
210
|
-
default_button: int = 0,
|
|
211
|
-
radio_buttons: Optional[List[Tuple[int, str]]] = None,
|
|
212
|
-
default_radio_button: int = 0,
|
|
213
|
-
verification_text: Optional[str] = None,
|
|
214
|
-
verification_checked_by_default: bool = False,
|
|
215
|
-
show_progress_bar: bool = False,
|
|
216
|
-
show_marquee_progress_bar: bool = False
|
|
217
|
-
):
|
|
218
|
-
"""初始化 TaskDialog 实例。
|
|
219
|
-
|
|
220
|
-
:param parent_hwnd: 父窗口的句柄。
|
|
221
|
-
:param title: 对话框窗口的标题。
|
|
222
|
-
:param main_instruction: 对话框的主要指令文本。
|
|
223
|
-
:param content: 对话框的详细内容文本。
|
|
224
|
-
:param common_buttons: 要显示的通用按钮。可以是以下两种形式之一:
|
|
225
|
-
1. TDCBF_* 常量的按位或组合 (例如 TDCBF_OK_BUTTON | TDCBF_CANCEL_BUTTON)
|
|
226
|
-
2. 字符串列表,支持 "ok", "yes", "no", "cancel", "retry", "close"
|
|
227
|
-
:param main_icon: 主图标。可以是以下几种形式之一:
|
|
228
|
-
1. TD_*_ICON 常量之一
|
|
229
|
-
2. HICON 句柄
|
|
230
|
-
3. 字符串:"warning", "error", "information", "shield"
|
|
231
|
-
:param footer: 页脚区域显示的文本。
|
|
232
|
-
:param custom_buttons: 自定义按钮列表。每个元组包含 (按钮ID, 按钮文本)。
|
|
233
|
-
:param default_button: 默认按钮的ID。可以是通用按钮ID (例如 IDOK) 或自定义按钮ID。
|
|
234
|
-
:param radio_buttons: 单选按钮列表。每个元组包含 (按钮ID, 按钮文本)。
|
|
235
|
-
:param default_radio_button: 默认选中的单选按钮的ID。
|
|
236
|
-
:param verification_text: 验证复选框的文本。如果为 None,则不显示复选框。
|
|
237
|
-
:param verification_checked_by_default: 验证复选框是否默认勾选。
|
|
238
|
-
:param show_progress_bar: 是否显示标准进度条。
|
|
239
|
-
:param show_marquee_progress_bar: 是否显示跑马灯式进度条。
|
|
240
|
-
"""
|
|
241
|
-
self.config = TASKDIALOGCONFIG()
|
|
242
|
-
self.config.cbSize = ctypes.sizeof(TASKDIALOGCONFIG)
|
|
243
|
-
self.config.hwndParent = parent_hwnd
|
|
244
|
-
self.config.dwFlags = TDF_ALLOW_DIALOG_CANCELLATION | TDF_POSITION_RELATIVE_TO_WINDOW
|
|
245
|
-
self.config.dwCommonButtons = self._process_common_buttons(common_buttons)
|
|
246
|
-
self.config.pszWindowTitle = title
|
|
247
|
-
self.config.pszMainInstruction = main_instruction
|
|
248
|
-
self.config.pszContent = content
|
|
249
|
-
self.config.pszFooter = footer
|
|
250
|
-
|
|
251
|
-
self.progress: int = 0
|
|
252
|
-
if show_progress_bar or show_marquee_progress_bar:
|
|
253
|
-
# 进度条暂时还没实现
|
|
254
|
-
raise NotImplementedError("Progress bar is not implemented yet.")
|
|
255
|
-
self.config.dwFlags |= TDF_CALLBACK_TIMER
|
|
256
|
-
if show_progress_bar:
|
|
257
|
-
self.config.dwFlags |= TDF_SHOW_PROGRESS_BAR
|
|
258
|
-
else:
|
|
259
|
-
self.config.dwFlags |= TDF_SHOW_MARQUEE_PROGRESS_BAR
|
|
260
|
-
|
|
261
|
-
# 将实例方法转为 C 回调函数指针。
|
|
262
|
-
# 必须将其保存为实例成员,否则会被垃圾回收!
|
|
263
|
-
self._callback_func_ptr = PFTASKDIALOGCALLBACK(self._callback)
|
|
264
|
-
self.config.pfCallback = self._callback_func_ptr
|
|
265
|
-
# 将本实例的id作为lpCallbackData传递,以便在回调中识别
|
|
266
|
-
self.config.lpCallbackData = id(self)
|
|
267
|
-
|
|
268
|
-
# --- 图标设置 ---
|
|
269
|
-
processed_icon = self._process_main_icon(main_icon)
|
|
270
|
-
if processed_icon is not None:
|
|
271
|
-
if isinstance(processed_icon, wintypes.LPWSTR):
|
|
272
|
-
self.config.pszMainIcon = processed_icon
|
|
273
|
-
else:
|
|
274
|
-
self.config.dwFlags |= TDF_USE_HICON_MAIN
|
|
275
|
-
self.config.hMainIcon = processed_icon
|
|
276
|
-
|
|
277
|
-
# --- 自定义按钮设置 ---
|
|
278
|
-
self.custom_buttons_list = []
|
|
279
|
-
if custom_buttons:
|
|
280
|
-
self.config.cButtons = len(custom_buttons)
|
|
281
|
-
button_array_type = TASKDIALOG_BUTTON * len(custom_buttons)
|
|
282
|
-
self.custom_buttons_list = button_array_type()
|
|
283
|
-
for i, (btn_id, btn_text) in enumerate(custom_buttons):
|
|
284
|
-
self.custom_buttons_list[i].nButtonID = btn_id
|
|
285
|
-
self.custom_buttons_list[i].pszButtonText = btn_text
|
|
286
|
-
self.config.pButtons = self.custom_buttons_list
|
|
287
|
-
|
|
288
|
-
if default_button:
|
|
289
|
-
self.config.nDefaultButton = default_button
|
|
290
|
-
|
|
291
|
-
# --- 单选按钮设置 ---
|
|
292
|
-
self.radio_buttons_list = []
|
|
293
|
-
if radio_buttons:
|
|
294
|
-
self.config.cRadioButtons = len(radio_buttons)
|
|
295
|
-
radio_array_type = TASKDIALOG_BUTTON * len(radio_buttons)
|
|
296
|
-
self.radio_buttons_list = radio_array_type()
|
|
297
|
-
for i, (btn_id, btn_text) in enumerate(radio_buttons):
|
|
298
|
-
self.radio_buttons_list[i].nButtonID = btn_id
|
|
299
|
-
self.radio_buttons_list[i].pszButtonText = btn_text
|
|
300
|
-
self.config.pRadioButtons = self.radio_buttons_list
|
|
301
|
-
|
|
302
|
-
if default_radio_button:
|
|
303
|
-
self.config.nDefaultRadioButton = default_radio_button
|
|
304
|
-
|
|
305
|
-
# --- 验证复选框设置 ---
|
|
306
|
-
if verification_text:
|
|
307
|
-
self.config.pszVerificationText = verification_text
|
|
308
|
-
if verification_checked_by_default:
|
|
309
|
-
self.config.dwFlags |= TDF_VERIFICATION_FLAG_CHECKED
|
|
310
|
-
|
|
311
|
-
def _process_common_buttons(self, common_buttons: int | List[CommonButtonLiteral]) -> int:
|
|
312
|
-
"""处理 common_buttons 参数,支持常量和字符串列表两种形式"""
|
|
313
|
-
if isinstance(common_buttons, int):
|
|
314
|
-
# 直接使用 Win32 常量
|
|
315
|
-
return common_buttons
|
|
316
|
-
elif isinstance(common_buttons, list):
|
|
317
|
-
# 处理字符串列表
|
|
318
|
-
result = 0
|
|
319
|
-
for button in common_buttons:
|
|
320
|
-
# 使用 match 和 assert_never 进行类型检查
|
|
321
|
-
match button:
|
|
322
|
-
case "ok":
|
|
323
|
-
result |= TDCBF_OK_BUTTON
|
|
324
|
-
case "yes":
|
|
325
|
-
result |= TDCBF_YES_BUTTON
|
|
326
|
-
case "no":
|
|
327
|
-
result |= TDCBF_NO_BUTTON
|
|
328
|
-
case "cancel":
|
|
329
|
-
result |= TDCBF_CANCEL_BUTTON
|
|
330
|
-
case "retry":
|
|
331
|
-
result |= TDCBF_RETRY_BUTTON
|
|
332
|
-
case "close":
|
|
333
|
-
result |= TDCBF_CLOSE_BUTTON
|
|
334
|
-
case _:
|
|
335
|
-
# 这在实际中不会发生,因为类型检查会阻止它
|
|
336
|
-
from typing import assert_never
|
|
337
|
-
assert_never(button)
|
|
338
|
-
return result
|
|
339
|
-
else:
|
|
340
|
-
raise TypeError("common_buttons must be either an int or a list of strings")
|
|
341
|
-
|
|
342
|
-
def _process_main_icon(self, main_icon: Optional[wintypes.LPWSTR | int | IconLiteral]) -> Optional[wintypes.LPWSTR | int]:
|
|
343
|
-
"""处理 main_icon 参数,支持常量和字符串两种形式"""
|
|
344
|
-
if main_icon is None:
|
|
345
|
-
return None
|
|
346
|
-
elif isinstance(main_icon, (wintypes.LPWSTR, int)):
|
|
347
|
-
# 直接使用 Win32 常量或 HICON 句柄
|
|
348
|
-
return main_icon
|
|
349
|
-
elif isinstance(main_icon, str):
|
|
350
|
-
# 处理字符串
|
|
351
|
-
match main_icon:
|
|
352
|
-
case "warning":
|
|
353
|
-
return TD_WARNING_ICON
|
|
354
|
-
case "error":
|
|
355
|
-
return TD_ERROR_ICON
|
|
356
|
-
case "information":
|
|
357
|
-
return TD_INFORMATION_ICON
|
|
358
|
-
case "shield":
|
|
359
|
-
return TD_SHIELD_ICON
|
|
360
|
-
case _:
|
|
361
|
-
# 这在实际中不会发生,因为类型检查会阻止它
|
|
362
|
-
from typing import assert_never
|
|
363
|
-
assert_never(main_icon)
|
|
364
|
-
else:
|
|
365
|
-
raise TypeError("main_icon must be None, a Windows constant, or a string")
|
|
366
|
-
|
|
367
|
-
def _callback(self, hwnd: wintypes.HWND, msg: int, wParam: int, lParam: int, lpRefData: int) -> int:
|
|
368
|
-
# 仅当 lpRefData 指向的是当前这个对象实例时才处理
|
|
369
|
-
if lpRefData != id(self):
|
|
370
|
-
return 0 # S_OK
|
|
371
|
-
|
|
372
|
-
if msg == TDN_TIMER:
|
|
373
|
-
# 更新进度条
|
|
374
|
-
if self.progress < 100:
|
|
375
|
-
self.progress += 5
|
|
376
|
-
# 发送消息给对话框来更新进度条位置
|
|
377
|
-
user32.SendMessageW(hwnd, TDM_SET_PROGRESS_BAR_POS, self.progress, 0)
|
|
378
|
-
else:
|
|
379
|
-
# 示例:进度达到100%后,可以模拟点击OK按钮关闭对话框
|
|
380
|
-
# from ctypes import wintypes
|
|
381
|
-
# user32.PostMessageW(hwnd, wintypes.UINT(1125), IDOK, 0) # TDM_CLICK_BUTTON
|
|
382
|
-
pass
|
|
383
|
-
|
|
384
|
-
elif msg == TDN_DESTROYED:
|
|
385
|
-
# 对话框已销毁
|
|
386
|
-
pass
|
|
387
|
-
|
|
388
|
-
return 0 # S_OK
|
|
389
|
-
|
|
390
|
-
def show(self) -> Tuple[int, int, bool]:
|
|
391
|
-
"""
|
|
392
|
-
显示对话框并返回用户交互的结果。
|
|
393
|
-
|
|
394
|
-
:return: 一个元组 (button_id, radio_button_id, verification_checked)
|
|
395
|
-
- button_id: 用户点击的按钮ID (例如 IDOK, IDCANCEL)。
|
|
396
|
-
- radio_button_id: 用户选择的单选按钮的ID。
|
|
397
|
-
- verification_checked: 验证复选框是否被勾选 (True/False)。
|
|
398
|
-
"""
|
|
399
|
-
pnButton = ctypes.c_int(0)
|
|
400
|
-
pnRadioButton = ctypes.c_int(0)
|
|
401
|
-
pfVerificationFlagChecked = wintypes.BOOL(False)
|
|
402
|
-
|
|
403
|
-
hr = TaskDialogIndirect(
|
|
404
|
-
ctypes.byref(self.config),
|
|
405
|
-
ctypes.byref(pnButton),
|
|
406
|
-
ctypes.byref(pnRadioButton),
|
|
407
|
-
ctypes.byref(pfVerificationFlagChecked)
|
|
408
|
-
)
|
|
409
|
-
|
|
410
|
-
if hr == 0: # S_OK
|
|
411
|
-
return pnButton.value, pnRadioButton.value, bool(pfVerificationFlagChecked.value)
|
|
412
|
-
else:
|
|
413
|
-
raise ctypes.WinError(hr)
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
# --- 示例用法 ---
|
|
417
|
-
if __name__ == '__main__':
|
|
418
|
-
|
|
419
|
-
print("--- 示例 1: 简单信息框 ---")
|
|
420
|
-
dlg_simple = TaskDialog(
|
|
421
|
-
title="操作成功",
|
|
422
|
-
main_instruction="您的操作已成功完成。",
|
|
423
|
-
content="文件已保存到您的文档目录。",
|
|
424
|
-
common_buttons=["ok"],
|
|
425
|
-
main_icon="information"
|
|
426
|
-
)
|
|
427
|
-
result_simple, _, _ = dlg_simple.show()
|
|
428
|
-
print(f"用户点击了按钮: {result_simple} (1=OK)\n")
|
|
429
|
-
|
|
430
|
-
print("--- 示例 2: 确认框 ---")
|
|
431
|
-
dlg_confirm = TaskDialog(
|
|
432
|
-
title="确认删除",
|
|
433
|
-
main_instruction="您确定要永久删除这个文件吗?",
|
|
434
|
-
content="这个操作无法撤销。文件将被立即删除。",
|
|
435
|
-
common_buttons=["yes", "no", "cancel"],
|
|
436
|
-
main_icon="warning",
|
|
437
|
-
default_button=IDNO
|
|
438
|
-
)
|
|
439
|
-
result_confirm, _, _ = dlg_confirm.show()
|
|
440
|
-
if result_confirm == IDYES:
|
|
441
|
-
print("用户选择了“是”。")
|
|
442
|
-
elif result_confirm == IDNO:
|
|
443
|
-
print("用户选择了“否”。")
|
|
444
|
-
elif result_confirm == IDCANCEL:
|
|
445
|
-
print("用户选择了“取消”。")
|
|
446
|
-
print(f"返回的按钮ID: {result_confirm}\n")
|
|
447
|
-
|
|
448
|
-
# 示例 3
|
|
449
|
-
print("--- 示例 3: 自定义按钮 ---")
|
|
450
|
-
CUSTOM_BUTTON_SAVE_ID = 101
|
|
451
|
-
CUSTOM_BUTTON_DONT_SAVE_ID = 102
|
|
452
|
-
my_buttons = [
|
|
453
|
-
(CUSTOM_BUTTON_SAVE_ID, "保存并退出"),
|
|
454
|
-
(CUSTOM_BUTTON_DONT_SAVE_ID, "不保存直接退出")
|
|
455
|
-
]
|
|
456
|
-
dlg_custom = TaskDialog(
|
|
457
|
-
title="未保存的更改",
|
|
458
|
-
main_instruction="文档中有未保存的更改,您想如何处理?",
|
|
459
|
-
custom_buttons=my_buttons,
|
|
460
|
-
common_buttons=["cancel"],
|
|
461
|
-
main_icon="warning",
|
|
462
|
-
footer="这是一个重要的提醒!"
|
|
463
|
-
)
|
|
464
|
-
result_custom, _, _ = dlg_custom.show()
|
|
465
|
-
if result_custom == CUSTOM_BUTTON_SAVE_ID:
|
|
466
|
-
print("用户选择了“保存并退出”。")
|
|
467
|
-
elif result_custom == CUSTOM_BUTTON_DONT_SAVE_ID:
|
|
468
|
-
print("用户选择了“不保存直接退出”。")
|
|
469
|
-
elif result_custom == IDCANCEL:
|
|
470
|
-
print("用户选择了“取消”。")
|
|
471
|
-
print(f"返回的按钮ID: {result_custom}\n")
|
|
472
|
-
|
|
473
|
-
# 示例 4: 带单选按钮和验证框的对话框
|
|
474
|
-
print("--- 示例 4: 单选按钮和验证框 ---")
|
|
475
|
-
RADIO_BTN_WORD_ID = 201
|
|
476
|
-
RADIO_BTN_EXCEL_ID = 202
|
|
477
|
-
RADIO_BTN_PDF_ID = 203
|
|
478
|
-
|
|
479
|
-
radio_buttons = [
|
|
480
|
-
(RADIO_BTN_WORD_ID, "保存为 Word 文档 (.docx)"),
|
|
481
|
-
(RADIO_BTN_EXCEL_ID, "保存为 Excel 表格 (.xlsx)"),
|
|
482
|
-
(RADIO_BTN_PDF_ID, "导出为 PDF 文档 (.pdf)")
|
|
483
|
-
]
|
|
484
|
-
|
|
485
|
-
dlg_radio = TaskDialog(
|
|
486
|
-
title="选择导出格式",
|
|
487
|
-
main_instruction="请选择您想要导出的文件格式。",
|
|
488
|
-
content="选择一个格式后,点击“确定”继续。",
|
|
489
|
-
common_buttons=["ok", "cancel"],
|
|
490
|
-
main_icon="information",
|
|
491
|
-
radio_buttons=radio_buttons,
|
|
492
|
-
default_radio_button=RADIO_BTN_PDF_ID, # 默认选中PDF
|
|
493
|
-
verification_text="设为我的默认导出选项",
|
|
494
|
-
verification_checked_by_default=True
|
|
495
|
-
)
|
|
496
|
-
btn_id, radio_id, checked = dlg_radio.show()
|
|
497
|
-
|
|
498
|
-
if btn_id == IDOK:
|
|
499
|
-
print(f"用户点击了“确定”。")
|
|
500
|
-
if radio_id == RADIO_BTN_WORD_ID:
|
|
501
|
-
print("选择了导出为 Word。")
|
|
502
|
-
elif radio_id == RADIO_BTN_EXCEL_ID:
|
|
503
|
-
print("选择了导出为 Excel。")
|
|
504
|
-
elif radio_id == RADIO_BTN_PDF_ID:
|
|
505
|
-
print("选择了导出为 PDF。")
|
|
506
|
-
|
|
507
|
-
if checked:
|
|
508
|
-
print("用户勾选了“设为我的默认导出选项”。")
|
|
509
|
-
else:
|
|
510
|
-
print("用户未勾选“设为我的默认导出选项”。")
|
|
511
|
-
else:
|
|
512
|
-
print("用户点击了“取消”。")
|
|
513
|
-
print(f"返回的按钮ID: {btn_id}, 单选按钮ID: {radio_id}, 验证框状态: {checked}\n")
|
|
1
|
+
"""
|
|
2
|
+
Windows Task Dialog interop module.
|
|
3
|
+
|
|
4
|
+
This module provides Windows TaskDialog functionality and is only available on Windows systems.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import platform
|
|
8
|
+
import warnings
|
|
9
|
+
|
|
10
|
+
from kotonebot.util import is_windows
|
|
11
|
+
|
|
12
|
+
# 检查是否在 Windows 平台上
|
|
13
|
+
if not is_windows():
|
|
14
|
+
_WINDOWS_ONLY_MSG = (
|
|
15
|
+
f"TaskDialog is only available on Windows systems. "
|
|
16
|
+
f"Current system: non-Windows\n"
|
|
17
|
+
"To use Windows TaskDialog features, please run this code on a Windows system."
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# 提供虚拟类以避免导入错误
|
|
22
|
+
class TaskDialog:
|
|
23
|
+
def __init__(self, *args, **kwargs):
|
|
24
|
+
raise ImportError(_WINDOWS_ONLY_MSG)
|
|
25
|
+
|
|
26
|
+
# 导出所有常量作为 None
|
|
27
|
+
__all__ = [
|
|
28
|
+
"TaskDialog",
|
|
29
|
+
"TDCBF_OK_BUTTON", "TDCBF_YES_BUTTON", "TDCBF_NO_BUTTON", "TDCBF_CANCEL_BUTTON",
|
|
30
|
+
"TDCBF_RETRY_BUTTON", "TDCBF_CLOSE_BUTTON",
|
|
31
|
+
"IDOK", "IDCANCEL", "IDABORT", "IDRETRY", "IDIGNORE", "IDYES", "IDNO", "IDCLOSE",
|
|
32
|
+
"TD_WARNING_ICON", "TD_ERROR_ICON", "TD_INFORMATION_ICON", "TD_SHIELD_ICON"
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
# 设置所有常量为 None 或保留为模块级变量
|
|
36
|
+
TDCBF_OK_BUTTON = TDCBF_YES_BUTTON = TDCBF_NO_BUTTON = TDCBF_CANCEL_BUTTON = None
|
|
37
|
+
TDCBF_RETRY_BUTTON = TDCBF_CLOSE_BUTTON = None
|
|
38
|
+
IDOK = IDCANCEL = IDABORT = IDRETRY = IDIGNORE = IDYES = IDNO = IDCLOSE = None
|
|
39
|
+
TD_WARNING_ICON = TD_ERROR_ICON = TD_INFORMATION_ICON = TD_SHIELD_ICON = None
|
|
40
|
+
|
|
41
|
+
# 阻止模块加载
|
|
42
|
+
raise ImportError(_WINDOWS_ONLY_MSG)
|
|
43
|
+
|
|
44
|
+
# 如果是 Windows,继续正常加载
|
|
45
|
+
import ctypes
|
|
46
|
+
from ctypes import wintypes
|
|
47
|
+
import time
|
|
48
|
+
from typing import List, Tuple, Optional
|
|
49
|
+
from typing import Literal
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
"TaskDialog",
|
|
53
|
+
"TDCBF_OK_BUTTON", "TDCBF_YES_BUTTON", "TDCBF_NO_BUTTON", "TDCBF_CANCEL_BUTTON",
|
|
54
|
+
"TDCBF_RETRY_BUTTON", "TDCBF_CLOSE_BUTTON",
|
|
55
|
+
"IDOK", "IDCANCEL", "IDABORT", "IDRETRY", "IDIGNORE", "IDYES", "IDNO", "IDCLOSE",
|
|
56
|
+
"TD_WARNING_ICON", "TD_ERROR_ICON", "TD_INFORMATION_ICON", "TD_SHIELD_ICON"
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
# --- Windows API 常量定义 ---
|
|
60
|
+
|
|
61
|
+
# 常用按钮
|
|
62
|
+
TDCBF_OK_BUTTON = 0x0001
|
|
63
|
+
TDCBF_YES_BUTTON = 0x0002
|
|
64
|
+
TDCBF_NO_BUTTON = 0x0004
|
|
65
|
+
TDCBF_CANCEL_BUTTON = 0x0008
|
|
66
|
+
TDCBF_RETRY_BUTTON = 0x0010
|
|
67
|
+
TDCBF_CLOSE_BUTTON = 0x0020
|
|
68
|
+
|
|
69
|
+
# 对话框返回值
|
|
70
|
+
IDOK = 1
|
|
71
|
+
IDCANCEL = 2
|
|
72
|
+
IDABORT = 3
|
|
73
|
+
IDRETRY = 4
|
|
74
|
+
IDIGNORE = 5
|
|
75
|
+
IDYES = 6
|
|
76
|
+
IDNO = 7
|
|
77
|
+
IDCLOSE = 8
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# 标准图标 (使用 MAKEINTRESOURCE 宏)
|
|
81
|
+
def MAKEINTRESOURCE(i: int) -> wintypes.LPWSTR:
|
|
82
|
+
return wintypes.LPWSTR(i)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
TD_WARNING_ICON = MAKEINTRESOURCE(65535)
|
|
86
|
+
TD_ERROR_ICON = MAKEINTRESOURCE(65534)
|
|
87
|
+
TD_INFORMATION_ICON = MAKEINTRESOURCE(65533)
|
|
88
|
+
TD_SHIELD_ICON = MAKEINTRESOURCE(65532)
|
|
89
|
+
|
|
90
|
+
# Task Dialog 标志
|
|
91
|
+
TDF_ENABLE_HYPERLINKS = 0x0001
|
|
92
|
+
TDF_USE_HICON_MAIN = 0x0002
|
|
93
|
+
TDF_USE_HICON_FOOTER = 0x0004
|
|
94
|
+
TDF_ALLOW_DIALOG_CANCELLATION = 0x0008
|
|
95
|
+
TDF_USE_COMMAND_LINKS = 0x0010
|
|
96
|
+
TDF_USE_COMMAND_LINKS_NO_ICON = 0x0020
|
|
97
|
+
TDF_EXPAND_FOOTER_AREA = 0x0040
|
|
98
|
+
TDF_EXPANDED_BY_DEFAULT = 0x0080
|
|
99
|
+
TDF_VERIFICATION_FLAG_CHECKED = 0x0100
|
|
100
|
+
TDF_SHOW_PROGRESS_BAR = 0x0200
|
|
101
|
+
TDF_SHOW_MARQUEE_PROGRESS_BAR = 0x0400
|
|
102
|
+
TDF_CALLBACK_TIMER = 0x0800
|
|
103
|
+
TDF_POSITION_RELATIVE_TO_WINDOW = 0x1000
|
|
104
|
+
TDF_RTL_LAYOUT = 0x2000
|
|
105
|
+
TDF_NO_DEFAULT_RADIO_BUTTON = 0x4000
|
|
106
|
+
TDF_CAN_BE_MINIMIZED = 0x8000
|
|
107
|
+
|
|
108
|
+
# Task Dialog 通知
|
|
109
|
+
TDN_CREATED = 0
|
|
110
|
+
TDN_NAVIGATED = 1
|
|
111
|
+
TDN_BUTTON_CLICKED = 2
|
|
112
|
+
TDN_HYPERLINK_CLICKED = 3
|
|
113
|
+
TDN_TIMER = 4
|
|
114
|
+
TDN_DESTROYED = 5
|
|
115
|
+
TDN_RADIO_BUTTON_CLICKED = 6
|
|
116
|
+
TDN_DIALOG_CONSTRUCTED = 7
|
|
117
|
+
TDN_VERIFICATION_CLICKED = 8
|
|
118
|
+
TDN_HELP = 9
|
|
119
|
+
TDN_EXPANDO_BUTTON_CLICKED = 10
|
|
120
|
+
|
|
121
|
+
# Windows 消息
|
|
122
|
+
WM_USER = 0x0400
|
|
123
|
+
TDM_SET_PROGRESS_BAR_POS = WM_USER + 114
|
|
124
|
+
|
|
125
|
+
CommonButtonLiteral = Literal["ok", "yes", "no", "cancel", "retry", "close"]
|
|
126
|
+
IconLiteral = Literal["warning", "error", "information", "shield"]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# --- C 结构体定义 (使用 ctypes) ---
|
|
130
|
+
|
|
131
|
+
class TASKDIALOG_BUTTON(ctypes.Structure):
|
|
132
|
+
_pack_ = 1
|
|
133
|
+
_fields_ = [("nButtonID", ctypes.c_int),
|
|
134
|
+
("pszButtonText", wintypes.LPCWSTR)]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# 定义回调函数指针原型
|
|
138
|
+
PFTASKDIALOGCALLBACK = ctypes.WINFUNCTYPE(
|
|
139
|
+
ctypes.HRESULT, # 返回值
|
|
140
|
+
wintypes.HWND, # hwnd
|
|
141
|
+
ctypes.c_uint, # msg
|
|
142
|
+
ctypes.c_size_t, # wParam
|
|
143
|
+
ctypes.c_size_t, # lParam
|
|
144
|
+
ctypes.c_ssize_t # lpRefData
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class TASKDIALOGCONFIG(ctypes.Structure):
|
|
149
|
+
_pack_ = 1
|
|
150
|
+
_fields_ = [
|
|
151
|
+
("cbSize", ctypes.c_uint),
|
|
152
|
+
("hwndParent", wintypes.HWND),
|
|
153
|
+
("hInstance", wintypes.HINSTANCE),
|
|
154
|
+
("dwFlags", ctypes.c_uint),
|
|
155
|
+
("dwCommonButtons", ctypes.c_uint),
|
|
156
|
+
("pszWindowTitle", wintypes.LPCWSTR),
|
|
157
|
+
("pszMainIcon", wintypes.LPCWSTR),
|
|
158
|
+
("pszMainInstruction", wintypes.LPCWSTR),
|
|
159
|
+
("pszContent", wintypes.LPCWSTR),
|
|
160
|
+
("cButtons", ctypes.c_uint),
|
|
161
|
+
("pButtons", ctypes.POINTER(TASKDIALOG_BUTTON)),
|
|
162
|
+
("nDefaultButton", ctypes.c_int),
|
|
163
|
+
("cRadioButtons", ctypes.c_uint),
|
|
164
|
+
("pRadioButtons", ctypes.POINTER(TASKDIALOG_BUTTON)),
|
|
165
|
+
("nDefaultRadioButton", ctypes.c_int),
|
|
166
|
+
("pszVerificationText", wintypes.LPCWSTR),
|
|
167
|
+
("pszExpandedInformation", wintypes.LPCWSTR),
|
|
168
|
+
("pszExpandedControlText", wintypes.LPCWSTR),
|
|
169
|
+
("pszCollapsedControlText", wintypes.LPCWSTR),
|
|
170
|
+
("pszFooterIcon", wintypes.LPCWSTR),
|
|
171
|
+
("pszFooter", wintypes.LPCWSTR),
|
|
172
|
+
("pfCallback", PFTASKDIALOGCALLBACK), # 使用定义好的原型
|
|
173
|
+
("lpCallbackData", ctypes.c_ssize_t),
|
|
174
|
+
("cxWidth", ctypes.c_uint)
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# --- 加载 comctl32.dll 并定义函数原型 ---
|
|
179
|
+
|
|
180
|
+
comctl32 = ctypes.WinDLL('comctl32')
|
|
181
|
+
user32 = ctypes.WinDLL('user32')
|
|
182
|
+
|
|
183
|
+
TaskDialogIndirect = comctl32.TaskDialogIndirect
|
|
184
|
+
TaskDialogIndirect.restype = ctypes.HRESULT
|
|
185
|
+
TaskDialogIndirect.argtypes = [
|
|
186
|
+
ctypes.POINTER(TASKDIALOGCONFIG),
|
|
187
|
+
ctypes.POINTER(ctypes.c_int),
|
|
188
|
+
ctypes.POINTER(ctypes.c_int),
|
|
189
|
+
ctypes.POINTER(wintypes.BOOL)
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# --- Python 封装类 ---
|
|
194
|
+
|
|
195
|
+
class TaskDialog:
|
|
196
|
+
"""
|
|
197
|
+
一个用于显示 Windows TaskDialog 的 Python 封装类。
|
|
198
|
+
支持自定义按钮、单选按钮、进度条、验证框等。
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
def __init__(self,
|
|
202
|
+
parent_hwnd: Optional[int] = None,
|
|
203
|
+
title: str = "Task Dialog",
|
|
204
|
+
main_instruction: str = "",
|
|
205
|
+
content: str = "",
|
|
206
|
+
common_buttons: int | List[CommonButtonLiteral] = TDCBF_OK_BUTTON,
|
|
207
|
+
main_icon: Optional[wintypes.LPWSTR | int | IconLiteral] = None,
|
|
208
|
+
footer: str = "",
|
|
209
|
+
custom_buttons: Optional[List[Tuple[int, str]]] = None,
|
|
210
|
+
default_button: int = 0,
|
|
211
|
+
radio_buttons: Optional[List[Tuple[int, str]]] = None,
|
|
212
|
+
default_radio_button: int = 0,
|
|
213
|
+
verification_text: Optional[str] = None,
|
|
214
|
+
verification_checked_by_default: bool = False,
|
|
215
|
+
show_progress_bar: bool = False,
|
|
216
|
+
show_marquee_progress_bar: bool = False
|
|
217
|
+
):
|
|
218
|
+
"""初始化 TaskDialog 实例。
|
|
219
|
+
|
|
220
|
+
:param parent_hwnd: 父窗口的句柄。
|
|
221
|
+
:param title: 对话框窗口的标题。
|
|
222
|
+
:param main_instruction: 对话框的主要指令文本。
|
|
223
|
+
:param content: 对话框的详细内容文本。
|
|
224
|
+
:param common_buttons: 要显示的通用按钮。可以是以下两种形式之一:
|
|
225
|
+
1. TDCBF_* 常量的按位或组合 (例如 TDCBF_OK_BUTTON | TDCBF_CANCEL_BUTTON)
|
|
226
|
+
2. 字符串列表,支持 "ok", "yes", "no", "cancel", "retry", "close"
|
|
227
|
+
:param main_icon: 主图标。可以是以下几种形式之一:
|
|
228
|
+
1. TD_*_ICON 常量之一
|
|
229
|
+
2. HICON 句柄
|
|
230
|
+
3. 字符串:"warning", "error", "information", "shield"
|
|
231
|
+
:param footer: 页脚区域显示的文本。
|
|
232
|
+
:param custom_buttons: 自定义按钮列表。每个元组包含 (按钮ID, 按钮文本)。
|
|
233
|
+
:param default_button: 默认按钮的ID。可以是通用按钮ID (例如 IDOK) 或自定义按钮ID。
|
|
234
|
+
:param radio_buttons: 单选按钮列表。每个元组包含 (按钮ID, 按钮文本)。
|
|
235
|
+
:param default_radio_button: 默认选中的单选按钮的ID。
|
|
236
|
+
:param verification_text: 验证复选框的文本。如果为 None,则不显示复选框。
|
|
237
|
+
:param verification_checked_by_default: 验证复选框是否默认勾选。
|
|
238
|
+
:param show_progress_bar: 是否显示标准进度条。
|
|
239
|
+
:param show_marquee_progress_bar: 是否显示跑马灯式进度条。
|
|
240
|
+
"""
|
|
241
|
+
self.config = TASKDIALOGCONFIG()
|
|
242
|
+
self.config.cbSize = ctypes.sizeof(TASKDIALOGCONFIG)
|
|
243
|
+
self.config.hwndParent = parent_hwnd
|
|
244
|
+
self.config.dwFlags = TDF_ALLOW_DIALOG_CANCELLATION | TDF_POSITION_RELATIVE_TO_WINDOW
|
|
245
|
+
self.config.dwCommonButtons = self._process_common_buttons(common_buttons)
|
|
246
|
+
self.config.pszWindowTitle = title
|
|
247
|
+
self.config.pszMainInstruction = main_instruction
|
|
248
|
+
self.config.pszContent = content
|
|
249
|
+
self.config.pszFooter = footer
|
|
250
|
+
|
|
251
|
+
self.progress: int = 0
|
|
252
|
+
if show_progress_bar or show_marquee_progress_bar:
|
|
253
|
+
# 进度条暂时还没实现
|
|
254
|
+
raise NotImplementedError("Progress bar is not implemented yet.")
|
|
255
|
+
self.config.dwFlags |= TDF_CALLBACK_TIMER
|
|
256
|
+
if show_progress_bar:
|
|
257
|
+
self.config.dwFlags |= TDF_SHOW_PROGRESS_BAR
|
|
258
|
+
else:
|
|
259
|
+
self.config.dwFlags |= TDF_SHOW_MARQUEE_PROGRESS_BAR
|
|
260
|
+
|
|
261
|
+
# 将实例方法转为 C 回调函数指针。
|
|
262
|
+
# 必须将其保存为实例成员,否则会被垃圾回收!
|
|
263
|
+
self._callback_func_ptr = PFTASKDIALOGCALLBACK(self._callback)
|
|
264
|
+
self.config.pfCallback = self._callback_func_ptr
|
|
265
|
+
# 将本实例的id作为lpCallbackData传递,以便在回调中识别
|
|
266
|
+
self.config.lpCallbackData = id(self)
|
|
267
|
+
|
|
268
|
+
# --- 图标设置 ---
|
|
269
|
+
processed_icon = self._process_main_icon(main_icon)
|
|
270
|
+
if processed_icon is not None:
|
|
271
|
+
if isinstance(processed_icon, wintypes.LPWSTR):
|
|
272
|
+
self.config.pszMainIcon = processed_icon
|
|
273
|
+
else:
|
|
274
|
+
self.config.dwFlags |= TDF_USE_HICON_MAIN
|
|
275
|
+
self.config.hMainIcon = processed_icon
|
|
276
|
+
|
|
277
|
+
# --- 自定义按钮设置 ---
|
|
278
|
+
self.custom_buttons_list = []
|
|
279
|
+
if custom_buttons:
|
|
280
|
+
self.config.cButtons = len(custom_buttons)
|
|
281
|
+
button_array_type = TASKDIALOG_BUTTON * len(custom_buttons)
|
|
282
|
+
self.custom_buttons_list = button_array_type()
|
|
283
|
+
for i, (btn_id, btn_text) in enumerate(custom_buttons):
|
|
284
|
+
self.custom_buttons_list[i].nButtonID = btn_id
|
|
285
|
+
self.custom_buttons_list[i].pszButtonText = btn_text
|
|
286
|
+
self.config.pButtons = self.custom_buttons_list
|
|
287
|
+
|
|
288
|
+
if default_button:
|
|
289
|
+
self.config.nDefaultButton = default_button
|
|
290
|
+
|
|
291
|
+
# --- 单选按钮设置 ---
|
|
292
|
+
self.radio_buttons_list = []
|
|
293
|
+
if radio_buttons:
|
|
294
|
+
self.config.cRadioButtons = len(radio_buttons)
|
|
295
|
+
radio_array_type = TASKDIALOG_BUTTON * len(radio_buttons)
|
|
296
|
+
self.radio_buttons_list = radio_array_type()
|
|
297
|
+
for i, (btn_id, btn_text) in enumerate(radio_buttons):
|
|
298
|
+
self.radio_buttons_list[i].nButtonID = btn_id
|
|
299
|
+
self.radio_buttons_list[i].pszButtonText = btn_text
|
|
300
|
+
self.config.pRadioButtons = self.radio_buttons_list
|
|
301
|
+
|
|
302
|
+
if default_radio_button:
|
|
303
|
+
self.config.nDefaultRadioButton = default_radio_button
|
|
304
|
+
|
|
305
|
+
# --- 验证复选框设置 ---
|
|
306
|
+
if verification_text:
|
|
307
|
+
self.config.pszVerificationText = verification_text
|
|
308
|
+
if verification_checked_by_default:
|
|
309
|
+
self.config.dwFlags |= TDF_VERIFICATION_FLAG_CHECKED
|
|
310
|
+
|
|
311
|
+
def _process_common_buttons(self, common_buttons: int | List[CommonButtonLiteral]) -> int:
|
|
312
|
+
"""处理 common_buttons 参数,支持常量和字符串列表两种形式"""
|
|
313
|
+
if isinstance(common_buttons, int):
|
|
314
|
+
# 直接使用 Win32 常量
|
|
315
|
+
return common_buttons
|
|
316
|
+
elif isinstance(common_buttons, list):
|
|
317
|
+
# 处理字符串列表
|
|
318
|
+
result = 0
|
|
319
|
+
for button in common_buttons:
|
|
320
|
+
# 使用 match 和 assert_never 进行类型检查
|
|
321
|
+
match button:
|
|
322
|
+
case "ok":
|
|
323
|
+
result |= TDCBF_OK_BUTTON
|
|
324
|
+
case "yes":
|
|
325
|
+
result |= TDCBF_YES_BUTTON
|
|
326
|
+
case "no":
|
|
327
|
+
result |= TDCBF_NO_BUTTON
|
|
328
|
+
case "cancel":
|
|
329
|
+
result |= TDCBF_CANCEL_BUTTON
|
|
330
|
+
case "retry":
|
|
331
|
+
result |= TDCBF_RETRY_BUTTON
|
|
332
|
+
case "close":
|
|
333
|
+
result |= TDCBF_CLOSE_BUTTON
|
|
334
|
+
case _:
|
|
335
|
+
# 这在实际中不会发生,因为类型检查会阻止它
|
|
336
|
+
from typing import assert_never
|
|
337
|
+
assert_never(button)
|
|
338
|
+
return result
|
|
339
|
+
else:
|
|
340
|
+
raise TypeError("common_buttons must be either an int or a list of strings")
|
|
341
|
+
|
|
342
|
+
def _process_main_icon(self, main_icon: Optional[wintypes.LPWSTR | int | IconLiteral]) -> Optional[wintypes.LPWSTR | int]:
|
|
343
|
+
"""处理 main_icon 参数,支持常量和字符串两种形式"""
|
|
344
|
+
if main_icon is None:
|
|
345
|
+
return None
|
|
346
|
+
elif isinstance(main_icon, (wintypes.LPWSTR, int)):
|
|
347
|
+
# 直接使用 Win32 常量或 HICON 句柄
|
|
348
|
+
return main_icon
|
|
349
|
+
elif isinstance(main_icon, str):
|
|
350
|
+
# 处理字符串
|
|
351
|
+
match main_icon:
|
|
352
|
+
case "warning":
|
|
353
|
+
return TD_WARNING_ICON
|
|
354
|
+
case "error":
|
|
355
|
+
return TD_ERROR_ICON
|
|
356
|
+
case "information":
|
|
357
|
+
return TD_INFORMATION_ICON
|
|
358
|
+
case "shield":
|
|
359
|
+
return TD_SHIELD_ICON
|
|
360
|
+
case _:
|
|
361
|
+
# 这在实际中不会发生,因为类型检查会阻止它
|
|
362
|
+
from typing import assert_never
|
|
363
|
+
assert_never(main_icon)
|
|
364
|
+
else:
|
|
365
|
+
raise TypeError("main_icon must be None, a Windows constant, or a string")
|
|
366
|
+
|
|
367
|
+
def _callback(self, hwnd: wintypes.HWND, msg: int, wParam: int, lParam: int, lpRefData: int) -> int:
|
|
368
|
+
# 仅当 lpRefData 指向的是当前这个对象实例时才处理
|
|
369
|
+
if lpRefData != id(self):
|
|
370
|
+
return 0 # S_OK
|
|
371
|
+
|
|
372
|
+
if msg == TDN_TIMER:
|
|
373
|
+
# 更新进度条
|
|
374
|
+
if self.progress < 100:
|
|
375
|
+
self.progress += 5
|
|
376
|
+
# 发送消息给对话框来更新进度条位置
|
|
377
|
+
user32.SendMessageW(hwnd, TDM_SET_PROGRESS_BAR_POS, self.progress, 0)
|
|
378
|
+
else:
|
|
379
|
+
# 示例:进度达到100%后,可以模拟点击OK按钮关闭对话框
|
|
380
|
+
# from ctypes import wintypes
|
|
381
|
+
# user32.PostMessageW(hwnd, wintypes.UINT(1125), IDOK, 0) # TDM_CLICK_BUTTON
|
|
382
|
+
pass
|
|
383
|
+
|
|
384
|
+
elif msg == TDN_DESTROYED:
|
|
385
|
+
# 对话框已销毁
|
|
386
|
+
pass
|
|
387
|
+
|
|
388
|
+
return 0 # S_OK
|
|
389
|
+
|
|
390
|
+
def show(self) -> Tuple[int, int, bool]:
|
|
391
|
+
"""
|
|
392
|
+
显示对话框并返回用户交互的结果。
|
|
393
|
+
|
|
394
|
+
:return: 一个元组 (button_id, radio_button_id, verification_checked)
|
|
395
|
+
- button_id: 用户点击的按钮ID (例如 IDOK, IDCANCEL)。
|
|
396
|
+
- radio_button_id: 用户选择的单选按钮的ID。
|
|
397
|
+
- verification_checked: 验证复选框是否被勾选 (True/False)。
|
|
398
|
+
"""
|
|
399
|
+
pnButton = ctypes.c_int(0)
|
|
400
|
+
pnRadioButton = ctypes.c_int(0)
|
|
401
|
+
pfVerificationFlagChecked = wintypes.BOOL(False)
|
|
402
|
+
|
|
403
|
+
hr = TaskDialogIndirect(
|
|
404
|
+
ctypes.byref(self.config),
|
|
405
|
+
ctypes.byref(pnButton),
|
|
406
|
+
ctypes.byref(pnRadioButton),
|
|
407
|
+
ctypes.byref(pfVerificationFlagChecked)
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
if hr == 0: # S_OK
|
|
411
|
+
return pnButton.value, pnRadioButton.value, bool(pfVerificationFlagChecked.value)
|
|
412
|
+
else:
|
|
413
|
+
raise ctypes.WinError(hr)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
# --- 示例用法 ---
|
|
417
|
+
if __name__ == '__main__':
|
|
418
|
+
|
|
419
|
+
print("--- 示例 1: 简单信息框 ---")
|
|
420
|
+
dlg_simple = TaskDialog(
|
|
421
|
+
title="操作成功",
|
|
422
|
+
main_instruction="您的操作已成功完成。",
|
|
423
|
+
content="文件已保存到您的文档目录。",
|
|
424
|
+
common_buttons=["ok"],
|
|
425
|
+
main_icon="information"
|
|
426
|
+
)
|
|
427
|
+
result_simple, _, _ = dlg_simple.show()
|
|
428
|
+
print(f"用户点击了按钮: {result_simple} (1=OK)\n")
|
|
429
|
+
|
|
430
|
+
print("--- 示例 2: 确认框 ---")
|
|
431
|
+
dlg_confirm = TaskDialog(
|
|
432
|
+
title="确认删除",
|
|
433
|
+
main_instruction="您确定要永久删除这个文件吗?",
|
|
434
|
+
content="这个操作无法撤销。文件将被立即删除。",
|
|
435
|
+
common_buttons=["yes", "no", "cancel"],
|
|
436
|
+
main_icon="warning",
|
|
437
|
+
default_button=IDNO
|
|
438
|
+
)
|
|
439
|
+
result_confirm, _, _ = dlg_confirm.show()
|
|
440
|
+
if result_confirm == IDYES:
|
|
441
|
+
print("用户选择了“是”。")
|
|
442
|
+
elif result_confirm == IDNO:
|
|
443
|
+
print("用户选择了“否”。")
|
|
444
|
+
elif result_confirm == IDCANCEL:
|
|
445
|
+
print("用户选择了“取消”。")
|
|
446
|
+
print(f"返回的按钮ID: {result_confirm}\n")
|
|
447
|
+
|
|
448
|
+
# 示例 3
|
|
449
|
+
print("--- 示例 3: 自定义按钮 ---")
|
|
450
|
+
CUSTOM_BUTTON_SAVE_ID = 101
|
|
451
|
+
CUSTOM_BUTTON_DONT_SAVE_ID = 102
|
|
452
|
+
my_buttons = [
|
|
453
|
+
(CUSTOM_BUTTON_SAVE_ID, "保存并退出"),
|
|
454
|
+
(CUSTOM_BUTTON_DONT_SAVE_ID, "不保存直接退出")
|
|
455
|
+
]
|
|
456
|
+
dlg_custom = TaskDialog(
|
|
457
|
+
title="未保存的更改",
|
|
458
|
+
main_instruction="文档中有未保存的更改,您想如何处理?",
|
|
459
|
+
custom_buttons=my_buttons,
|
|
460
|
+
common_buttons=["cancel"],
|
|
461
|
+
main_icon="warning",
|
|
462
|
+
footer="这是一个重要的提醒!"
|
|
463
|
+
)
|
|
464
|
+
result_custom, _, _ = dlg_custom.show()
|
|
465
|
+
if result_custom == CUSTOM_BUTTON_SAVE_ID:
|
|
466
|
+
print("用户选择了“保存并退出”。")
|
|
467
|
+
elif result_custom == CUSTOM_BUTTON_DONT_SAVE_ID:
|
|
468
|
+
print("用户选择了“不保存直接退出”。")
|
|
469
|
+
elif result_custom == IDCANCEL:
|
|
470
|
+
print("用户选择了“取消”。")
|
|
471
|
+
print(f"返回的按钮ID: {result_custom}\n")
|
|
472
|
+
|
|
473
|
+
# 示例 4: 带单选按钮和验证框的对话框
|
|
474
|
+
print("--- 示例 4: 单选按钮和验证框 ---")
|
|
475
|
+
RADIO_BTN_WORD_ID = 201
|
|
476
|
+
RADIO_BTN_EXCEL_ID = 202
|
|
477
|
+
RADIO_BTN_PDF_ID = 203
|
|
478
|
+
|
|
479
|
+
radio_buttons = [
|
|
480
|
+
(RADIO_BTN_WORD_ID, "保存为 Word 文档 (.docx)"),
|
|
481
|
+
(RADIO_BTN_EXCEL_ID, "保存为 Excel 表格 (.xlsx)"),
|
|
482
|
+
(RADIO_BTN_PDF_ID, "导出为 PDF 文档 (.pdf)")
|
|
483
|
+
]
|
|
484
|
+
|
|
485
|
+
dlg_radio = TaskDialog(
|
|
486
|
+
title="选择导出格式",
|
|
487
|
+
main_instruction="请选择您想要导出的文件格式。",
|
|
488
|
+
content="选择一个格式后,点击“确定”继续。",
|
|
489
|
+
common_buttons=["ok", "cancel"],
|
|
490
|
+
main_icon="information",
|
|
491
|
+
radio_buttons=radio_buttons,
|
|
492
|
+
default_radio_button=RADIO_BTN_PDF_ID, # 默认选中PDF
|
|
493
|
+
verification_text="设为我的默认导出选项",
|
|
494
|
+
verification_checked_by_default=True
|
|
495
|
+
)
|
|
496
|
+
btn_id, radio_id, checked = dlg_radio.show()
|
|
497
|
+
|
|
498
|
+
if btn_id == IDOK:
|
|
499
|
+
print(f"用户点击了“确定”。")
|
|
500
|
+
if radio_id == RADIO_BTN_WORD_ID:
|
|
501
|
+
print("选择了导出为 Word。")
|
|
502
|
+
elif radio_id == RADIO_BTN_EXCEL_ID:
|
|
503
|
+
print("选择了导出为 Excel。")
|
|
504
|
+
elif radio_id == RADIO_BTN_PDF_ID:
|
|
505
|
+
print("选择了导出为 PDF。")
|
|
506
|
+
|
|
507
|
+
if checked:
|
|
508
|
+
print("用户勾选了“设为我的默认导出选项”。")
|
|
509
|
+
else:
|
|
510
|
+
print("用户未勾选“设为我的默认导出选项”。")
|
|
511
|
+
else:
|
|
512
|
+
print("用户点击了“取消”。")
|
|
513
|
+
print(f"返回的按钮ID: {btn_id}, 单选按钮ID: {radio_id}, 验证框状态: {checked}\n")
|