前阵子一直在用某个桌面工具,它有个每日签到领积分的机制。操作倒也不复杂——打开窗口、点头像、点签到按钮、关弹窗,拢共四步。可问题在于,每天都要重复这一套,时间一长,偶尔还真会忘了签。
碰巧当时有个硬性要求:不能装任何第三方库,pyautogui、pynput 这些统统靠边站,纯标准库把桌面自动化搞定。
这篇文章就分享一下,如何用 ctypes 直接调用 Win32 API,写一个不到 100 行的签到脚本,再通过系统计划任务实现每天 8:00 自动打卡。
方案选型
桌面自动化在 Python 生态里,通常有三条可选的路:
| 方案 | 代表库 | 优点 | 缺点 |
|---|---|---|---|
| 图像识别 | pyautogui | 直观,所见即所得 | 依赖屏幕分辨率,需额外安装 |
| UI 自动化 | uiautomation | 精确定位控件 | 依赖 UI Automation 框架,Electron 应用可能不暴露控件树 |
| Win32 API | ctypes | 零依赖,标准库自带 | 只能基于坐标,不同分辨率需重新校准 |
我的目标程序是一个基于 Electron 的桌面应用。尝试用 UIA 枚举控件树时发现——空的。Electron 默认不开启无障碍支持,这条路直接堵死。所以方案最终锁定在 Win32 API 的坐标点击上。
核心技术栈
全部通过 ctypes.windll.user32 调用,不装任何第三方包:
- 窗口查找:
FindWindowW— 通过窗口标题获取句柄 - 窗口激活:
ShowWindow/SetForegroundWindow/BringWindowToTop— 三步确保窗口可见并获得焦点 - 鼠标模拟:
SetCursorPos+mouse_event— 移动光标并执行左键点击 - 键盘模拟:
keybd_event— 模拟 ESC 关闭弹窗
完整代码
关键技术点详解
1. FindWindowW — 按标题精准定位
hwnd = user32.FindWindowW(None, "目标窗口标题")
第一个参数是窗口类名(传 None 表示不限制),第二个是窗口标题。这比遍历所有窗口再匹配标题高效得多。
注意:有些应用窗口标题会动态变化(比如浏览器标签页),这时需要先通过进程名找到 PID,再枚举窗口。
2. ShowWindow 的参数含义
user32.ShowWindow(hwnd, 9) # SW_RESTORE
常用值:
1= SW_SHOWNORMAL(正常显示)3= SW_MAXIMIZE(最大化)6= SW_MINIMIZE(最小化)9= SW_RESTORE(恢复原始大小和位置)
这里用 SW_RESTORE 是为了确保窗口从最小化状态恢复,但不强制最大化——如果用户手动调整过窗口大小,不会被打乱。
3. mouse_event 模拟点击 vs SetCursorPos
def click(x, y, delay=0.15):
user32.SetCursorPos(x, y) # 移动光标到目标位置
time.sleep(0.1) # 等待系统响应
user32.mouse_event(0x0002, ...) # 左键按下
time.sleep(0.08)
user32.mouse_event(0x0004, ...) # 左键释放
SetCursorPos 只负责移动光标,不会触发点击事件。必须配合 mouse_event 的 DOWN/UP 组合才能真正模拟一次点击。
为什么要加 sleep? 去掉 time.sleep 后,点击事件太快,目标应用可能来不及响应。0.08~0.15 秒是一个经过测试的稳定值,比这个快容易出现“点了但没反应”的问题。
4. keybd_event 的参数
user32.keybd_event(0x1B, 0, 0, 0) # 按下
user32.keybd_event(0x1B, 0, 2, 0) # 释放
- 第一个参数:虚拟键码(VK_ESCAPE = 0x1B)
- 第二个参数:硬件扫描码,传 0 即可
- 第三个参数:0 = 按下,2 = KEYEVENTF_KEYUP(释放)
- 第四个参数:额外数据,传 0
重要:按下和释放必须成对出现,否则目标程序会认为按键一直处于按下状态,后续键盘操作都可能异常。
5. Electron 应用的窗口激活陷阱
Electron 应用有个常见问题——窗口可能处于离屏渲染状态。具体表现是 GetWindowRect 返回的坐标可能是 (-25600, -25600),但窗口在屏幕上确实可见。
这是因为 Electron 使用 Direct3D 渲染时,会将一个离屏表面映射到屏幕坐标。好在这类应用对 SetForegroundWindow 和 BringWindowToTop 的响应很直接——它们不依赖窗口坐标,而是直接操作窗口 Z 序,所以不影响激活效果。
踩坑记录
坑 1:弹窗出现后窗口失焦
点击头像弹出菜单后,菜单本身是一个独立的浮层。如果中间不加 SetForegroundWindow 重新激活,后续的 mouse_event 点击会落在菜单外的空白区域。
解决:弹窗出现后、点击签到按钮前,再次调用 BringWindowToTop + SetForegroundWindow。
坑 2:锁屏状态下无效
SetCursorPos 和 mouse_event 在锁屏状态下会被系统拦截。如果你用的是系统计划任务,务必勾选“只在用户登录时运行”,而不是“不管用户是否登录”。
坑 3:坐标校准的通用性
坐标写死后只适用于当前分辨率。如果需要支持不同分辨率,可以:
import ctypes
screen_w = user32.GetSystemMetrics(0) # SM_CXSCREEN
screen_h = user32.GetSystemMetrics(1) # SM_CYSCREEN
MENU_X = int(60 * screen_w / 1536)
部署为定时任务
Windows 下两种方式:
方式一:任务计划程序(推荐)
schtasks /create /tn "桌面签到" /tr "python C:\scripts\checkin.py" /sc daily /st 08:00
方式二:工具自带定时任务
如果你的桌面工具本身支持定时任务调度,直接用它调度即可,省去系统计划任务的配置。
总结
这个方案的亮点在于:
- 零依赖——只用了 Python 标准库,不需要
pip install任何东西 - 原理透明——直接操作 Win32 API,每一步都清楚发生了什么
- 稳定可靠——加了充分的 sleep 等待和窗口重激活逻辑,跑了几天没出过问题
- 可移植——换一个目标应用,只需要改窗口标题和几个坐标值
局限性也很明显:坐标写死,换电脑或换分辨率要重新校准。如果你追求更强的通用性,可以考虑配合 OpenCV 做模板匹配定位按钮,但那又是另一篇文章了。
