1. 从一次真实的 VBA 自动化需求说起
VBA 模拟键盘鼠标操作与窗口激活,本质上是让 Excel 宏或 Word 宏具备「操作其他程序」的能力。你可能会遇到这样的场景:每天要从某个内部系统导出报表,系统只提供桌面客户端,没有 API;或者需要批量把数据填进一个老旧的录入工具,那个工具连导入功能都没有。这时候 Shell 启动外部程序、AppActivate 激活目标窗口、再用 SendKeys 和 mouse_event 模拟键盘鼠标,就是最直接的解法。
这套方案适合谁?适合已经会用 VBA 写基础宏、但卡在「怎么让宏去点别的窗口」这一步的办公自动化开发者。它不需要你懂 C++ 或驱动开发,只要理解 Windows 窗口句柄、焦点、消息队列这几个概念就能跑通。我试过在财务对账、批量打印、数据搬运这几类任务里用这套组合,实测下来最关键的难点不在代码本身,而在窗口激活的时机控制和权限环境。
这篇文章会先讲清楚 Shell 和 AppActivate 的配合逻辑,然后给出 TaoToken 统一 Key/API 通道的 config.toml 骨架与 settings.json 配置片段,让需要调用大模型做文本处理或决策的环节也能纳入同一条通道。接着演示一次可复制的窗口激活加按键发送验证动作,最后把常见的报错和排查路径列出来。你跟着做,能在本地快速跑通整个流程。
2. TaoToken 前置:统一 Key 与 API 通道准备
在 VBA 里做窗口自动化,很多时候不只是「按键」,还需要在按键前后调用模型做内容生成、格式判断或异常识别。比如把一段文本发给模型做摘要,再把摘要粘贴进目标窗口。这时候如果每个模型都单独配 Key、单独改 endpoint,维护成本会很高。TaoToken 的思路是提供一个统一的 API 通道,你用同一个 Key 就能切换不同模型,配置集中在一个文件里。
先到官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 了解通道能力,然后进控制台创建 API Key。API 基地址是 https://taotoken.net/api ,注意这个地址不加 UTM 参数,直接用于程序请求。Key 创建入口在 https://taotoken.net/api-keys ,建议按项目建不同 Key,方便后续排查是哪个脚本在调用。
拿到 Key 之后,本地配置分两块:一块是给命令行工具或 SDK 用的 config.toml,一块是给编辑器或插件用的 settings.json。下面给出骨架,你按自己的模型名和参数替换即可。
2.1 config.toml 骨架
# TaoToken 统一通道配置骨架 # 适用于命令行工具、SDK、脚本调用 [default] api_base = "https://taotoken.net/api" api_key = "sk-你的Key" timeout = 60 [models] # 对话与文本处理 chat_model = "claude-sonnet" # 代码补全与长上下文 code_model = "claude-code" # 轻量判断任务 fast_model = "gpt-4o-mini" [retry] max_attempts = 3 backoff_seconds = 2这个骨架里 api_base 固定指向 TaoToken 的 API 地址,api_key 换成你在控制台生成的那串。models 段按用途分了三类,实际调用时用哪类就填哪类。retry 段是给网络抖动准备的,VBA 里调用 HTTP 接口时也建议做重试。
2.2 settings.json 配置片段
{ "taotoken": { "apiBase": "https://taotoken.net/api", "apiKey": "sk-你的Key", "defaultModel": "claude-sonnet", "models": { "chat": "claude-sonnet", "code": "claude-code", "fast": "gpt-4o-mini" }, "requestOptions": { "timeout": 60000, "maxRetries": 3 } } }settings.json 适合编辑器插件或带配置文件的客户端读取。两个文件里的 Key 是同一个,模型名按你实际开通的填。如果你只是做纯窗口自动化、暂时不调模型,这两块可以先放着,等需要文本处理时再接入。
3. 可复制配置:Shell 启动、AppActivate 激活与键鼠模拟
这一节是核心操作部分。先讲 Shell 的第二个参数怎么选,再讲 AppActivate 的两种传参方式,最后把 mouse_event 和 SendKeys 的组合写成一个可运行的 Sub。
3.1 Shell 第二个参数对照
Shell 的第一个参数是程序路径或命令,第二个参数决定窗口显示状态和焦点。下面这张表直接对照常数值和效果:
| 常数值 | 描述 | 焦点行为 |
|---|---|---|
| vbHide (0) | 窗口隐藏 | 焦点传给隐藏窗口 |
| vbNormalFocus (1) | 正常大小 | 窗口获得焦点 |
| vbMinimizedFocus (2) | 最小化为图符 | 图符获得焦点 |
| vbMaximizedFocus (3) | 最大化 | 窗口获得焦点 |
| vbNormalNoFocus (4) | 恢复最近大小位置 | 当前活动窗口不变 |
| vbMinimizedNoFocus (6) | 最小化为图符 | 当前活动窗口不变 |
做自动化时,如果你希望启动后立刻能 SendKeys,用 1 或 3;如果你只是后台启动、稍后再激活,用 4 或 6 避免抢焦点。Shell 的返回值是进程 ID,这个 ID 要存下来,后面 AppActivate 会用到。
3.2 AppActivate 的两种传参
AppActivate 可以接受窗口标题字符串,也可以接受 Shell 返回的进程 ID。用标题的好处是能激活本来就打开的窗口,坏处是标题可能变化或被截断。用进程 ID 的好处是精确,但只能激活 VBA 自己用 Shell 启动并拿到返回值的窗口。
' 方式一:按标题激活 AppActivate "1.txt - 记事本" ' 方式二:按 Shell 返回值激活 Dim pid As Double pid = Shell("notepad", 1) AppActivate pid, False第二个参数 False 表示不等待激活完成就继续执行。实际用的时候建议配合 Application.Wait 给窗口一点时间,否则 SendKeys 可能发到错误的窗口。
3.3 完整的窗口激活加按键发送验证
下面这段代码打开三个记事本,各写一点内容,然后按打开顺序关闭且不保存。你可以直接复制到 VBA 模块里运行。
Option Explicit ' 声明鼠标事件与光标位置 API Public Declare Sub mouse_event Lib "user32" (ByVal dwFlags As Long, _ ByVal dx As Long, ByVal dy As Long, ByVal cButtons As Long, ByVal dwExtraInfo As Long) Public Declare Function GetCursorPos Lib "user32" (lpPoint As POINTAPI) As Long Public Declare Function SetCursorPos Lib "user32" (ByVal x As Long, ByVal y As Long) As Long Public Const MOUSEEVENTF_MOVE = &H1 Public Const MOUSEEVENTF_LEFTDOWN = &H2 Public Const MOUSEEVENTF_LEFTUP = &H4 Public Const MOUSEEVENTF_RIGHTDOWN = &H8 Public Const MOUSEEVENTF_RIGHTUP = &H10 Public Const MOUSEEVENTF_ABSOLUTE = &H8000 Type POINTAPI x As Long y As Long End Type ' 验证动作:打开三个记事本,写入内容,按顺序关闭不保存 Sub testAppActivate() Dim windowCodeList As New Collection Dim i As Variant Dim pid As Double ' 启动三个记事本并记录进程 ID For i = 1 To 3 pid = Shell("notepad", 1) Application.Wait (Now + TimeValue("0:00:01")) Application.SendKeys "窗口" & i & "的内容" windowCodeList.Add pid Debug.Print "启动进程 ID: " & pid Next i ' 按打开顺序激活并关闭 For Each i In windowCodeList Debug.Print "激活进程 ID: " & i AppActivate i, False Application.Wait (Now + TimeValue("0:00:01")) ' Alt+F4 关闭窗口 Application.SendKeys "%{f4}" Application.Wait (Now + TimeValue("0:00:01")) ' 按 N 不保存 Application.SendKeys "n" Application.Wait (Now + TimeValue("0:00:01")) Next i MsgBox "三个记事本已按顺序关闭且未保存" End Sub ' 鼠标点击验证:在当前位置左键双击 Sub testMouseClick() Dim Cp As POINTAPI GetCursorPos Cp SetCursorPos Cp.x, Cp.y Dim i As Integer For i = 1 To 2 mouse_event MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0 mouse_event MOUSEEVENTF_LEFTUP, 0, 0, 0, 0 Application.Wait (Now + TimeValue("0:00:01")) Next i Application.SendKeys "abc" Application.Wait (Now + TimeValue("0:00:01")) Application.SendKeys "^s" End Sub运行 testAppActivate 时,你会看到三个记事本依次打开、写入内容、然后被 Alt+F4 关闭并选择不保存。Debug.Print 会在立即窗口输出进程 ID,方便你对照。testMouseClick 则是在当前光标位置双击,然后输入 abc 并 Ctrl+S 保存,适合验证鼠标事件是否生效。
4. 验证请求与成功结果
跑完上面的代码,怎么确认真的成功了?看三个地方。
第一,立即窗口的 Debug.Print 输出。正常情况你会看到三行「启动进程 ID」和三行「激活进程 ID」,数字一一对应。如果某个进程 ID 激活失败,AppActivate 会抛错,你能立刻定位是哪个窗口出了问题。
第二,记事本的实际行为。三个窗口应该按打开顺序依次被关闭,每次关闭时弹出「是否保存」对话框,然后被 SendKeys "n" 选择不保存。如果某个窗口没被关闭,说明 AppActivate 没找到它,或者焦点被其他窗口抢走了。
第三,鼠标点击验证。运行 testMouseClick 后,当前光标位置应该出现双击效果,然后输入 abc。如果你在记事本里运行,会看到 abc 被输入,Ctrl+S 触发保存对话框。
如果你需要把模型调用也串进来,可以在写入内容前先请求 TaoToken 的模型对话接口,把返回文本用 SendKeys 发出去。模型对话入口在 https://taotoken.net/api?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,你可以先用它验证 Key 和通道是否正常,再接入 VBA 的 HTTP 请求。
5. 本篇常见错排查
5.1 AppActivate 报错「找不到窗口」
最常见的原因是窗口标题不匹配或进程 ID 失效。用标题激活时,标题必须和任务栏显示的完全一致,包括空格和连字符。用进程 ID 激活时,确认 Shell 返回值是 Double 类型,并且窗口还没被关闭。如果窗口启动慢,AppActivate 执行时窗口还没创建,就会报错。解决办法是在 Shell 之后加 Application.Wait 等待一到两秒。
5.2 SendKeys 发到了错误的窗口
SendKeys 是发给当前活动窗口的,如果 AppActivate 没成功,按键就发到别处了。排查方法是每次 SendKeys 前先确认 AppActivate 的返回值,或者用 AppActivate 的第二个参数设为 True 等待激活完成。另外,如果目标程序以管理员权限运行,而 VBA 宿主不是管理员,SendKeys 会被 UIPI 机制拦截,表现为按键无效但不报错。这种情况需要让 VBA 宿主和目标程序在同一权限级别运行。
5.3 mouse_event 点击位置不对
mouse_event 的坐标是屏幕绝对坐标,不是窗口相对坐标。如果你用 SetCursorPos 设置了位置,再调用 mouse_event,点击会发生在设置的位置。但如果你用了 MOUSEEVENTF_ABSOLUTE 标志,坐标需要归一化到 0 到 65535 范围。建议先用 GetCursorPos 拿到当前位置,再决定是否移动。另外,高 DPI 缩放下坐标可能偏移,需要在系统设置里确认缩放比例。
5.4 Shell 启动的程序没有窗口
有些程序启动后不显示窗口,或者窗口标题为空。这时候 AppActivate 用标题找不到,用进程 ID 也可能因为窗口句柄未就绪而失败。可以先用任务管理器确认进程是否真的在运行,再用 Spy++ 或类似工具查看窗口句柄。如果程序本身是后台服务,那就不适合用 AppActivate,应该考虑其他集成方式。
5.5 模型调用返回 401 或超时
如果你在 VBA 里通过 HTTP 调用 TaoToken 接口,返回 401 说明 Key 不对或没带上。检查 config.toml 或 settings.json 里的 api_key 是否和 https://taotoken.net/api-keys 里创建的一致。超时的话,把 timeout 调大,或者检查网络是否能访问 https://taotoken.net/api 。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,里面有各语言的请求示例,可以对照排查。
6. 继续跑通你的自动化流程
窗口激活和键鼠模拟跑通之后,下一步通常是把重复操作封装成可复用的函数,加上错误处理和日志。如果你要做的是长期运行的编码或 Agent 任务,建议把模型调用统一走 TaoToken 的 Coding Plan,入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,这样 Key 和通道管理集中在一处,换模型不用改代码。
实际用的时候,我习惯把每个自动化步骤的进程 ID 和窗口标题记到一张隐藏工作表里,出错时能快速回溯是哪一步的窗口没激活。另外,SendKeys 的字符串里如果包含特殊字符,比如加号、百分号、波浪号,需要用大括号转义,否则会被解释成功能键。这些细节在批量操作时特别容易踩坑,建议先在测试环境跑几轮再上生产。