为什么 FindWindowW("Progman", "Program Manager") 总是返回空句柄?
结论
先修正类型声明:FindWindowW 要求 UTF-16 字符串,Koffi 的 str 使用 UTF-8,因此参数应改为 str16,返回值也应从 int 改为指针类型。查找时先用 FindWindowW('Progman', null),排除窗口标题不匹配的影响。
返回 0 还不足以说明系统中没有 Progman 窗口。修正 FFI 声明后,再检查实际窗口和运行环境。
原声明有哪些问题?
原声明是:
const FindWindowW = user32.func(
"int FindWindowW(str lpClassNmae, str lpWindowName)"
);
字符串编码与 W 版本不匹配
FindWindowW 的原生签名为:
HWND FindWindowW(
LPCWSTR lpClassName,
LPCWSTR lpWindowName
);
两个参数都指向以空字符结尾的 UTF-16 字符串。在 Koffi 中,str 对应 UTF-8,str16 对应 UTF-16。即使 'Progman' 只包含 ASCII 字符,两种编码的内存布局也不同,不能混用。Koffi 字符串类型说明
HWND 不能声明为 int
HWND 是指针宽度的句柄类型,int 则固定为 32 位。在 64 位进程中,用 int 接收它会丢失高位。这不一定会让返回值始终为零,但可能导致后续 API 收到错误的句柄。Windows 数据类型说明
声明中还应显式使用 __stdcall,与 Win32 API 的调用约定一致。在 32 位进程中尤其需要注意这一点。Koffi 调用约定说明
lpClassNmae 只是参数名拼写有误,不影响这里的参数传递。
先用最小代码获取句柄
import koffi from 'koffi';
const user32 = koffi.load('user32.dll');
// 用 void * 承载 HWND,避免按 32 位 int 接收。
const FindWindowW = user32.func(
'void * __stdcall FindWindowW(str16 lpClassName, str16 lpWindowName)'
);
const progman = FindWindowW('Progman', null);
console.log('Progman:', progman);
if (!progman) {
throw new Error('未找到 Progman 窗口');
}
第二个参数设为 null 时,只按类名查找,不限制窗口标题。如果传入 "Program Manager",类名和标题就必须同时匹配,而这个标题并非在所有环境下都适用。null 与空字符串 '' 的含义也不同。FindWindowW 参数说明
将 Koffi 返回的句柄值直接传给后续 API,不要为了输出日志或统一类型而强制转成 Number。这里返回的是指针,判断空句柄时,使用 if (!progman) 比只比较 progman === 0 更合适。
修正后仍然找不到,按什么顺序检查?
-
先检查标题条件。使用修正后的同一声明,分别调用:
console.log('按类名:', FindWindowW('Progman', null)); console.log( '按类名和标题:', FindWindowW('Progman', 'Program Manager') );如果前者成功、后者失败,就说明
"Program Manager"这个标题条件没有匹配。 -
用 Spy++ 的窗口树或搜索功能,按类名
Progman查找,核对实际标题和句柄。也可以通过EnumWindows枚举顶层窗口,再用GetClassNameW、GetWindowTextW查看属性。FindWindowW不搜索子窗口。Spy++ 窗口查看说明、FindWindowW 查找范围 -
检查 Explorer 桌面 Shell 是否已正常启动,以及程序是否运行在目标用户的交互会话和桌面中。如果程序由服务、其他用户会话或非交互任务启动,先在当前桌面的普通终端中运行最小示例,比较结果。Windows 的 desktop 是有隔离边界的系统对象,不同运行环境未必共享同一组窗口。Windows desktop 说明
查找失败后,紧接着调用 GetLastError() 也无法解释这次失败。微软文档明确说明,FindWindowW 不修改 last-error 值,因此读到的可能是之前其他调用留下的错误。FindWindowW 返回值说明
后续 SendMessageTimeoutW 也要修正类型
拿到 progman 后,后续 API 声明中的句柄和参数也要使用正确的类型宽度。SendMessageTimeoutW 的输出参数是 DWORD_PTR *,可以用 uintptr_t * 表示;WPARAM、LPARAM 和返回值也应使用指针宽度的整数类型。
将下面的代码接在前面的查找示例之后,可演示消息调用:
const SendMessageTimeoutW = user32.func(`
intptr_t __stdcall SendMessageTimeoutW(
void *hWnd,
uint32_t Msg,
uintptr_t wParam,
intptr_t lParam,
uint32_t fuFlags,
uint32_t uTimeout,
_Out_ uintptr_t *lpdwResult
)
`);
const WM_SPAWN_WORKERW = 0x052C;
const SMTO_NORMAL = 0x0000;
const messageResult: Array<number | bigint> = [0];
const status = SendMessageTimeoutW(
progman,
WM_SPAWN_WORKERW,
0,
0,
SMTO_NORMAL,
1000,
messageResult
);
if (!status) {
throw new Error('SendMessageTimeoutW 失败或超时');
}
console.log('消息处理结果:', messageResult[0]);
status 表示 API 调用是否成功,messageResult[0] 是目标窗口处理消息后的结果,两者含义不同。消息发送成功,也不代表壁纸挂载已经完成。SendMessageTimeoutW 文档
这里通过 _Out_ 和单元素数组接收结果,无需手动分配内存。如果继续使用 koffi.alloc(),应写成:
const resultPtr = koffi.alloc('uintptr_t', 1);
第二个参数表示元素数量,所以原来的 koffi.alloc('int', koffi.sizeof('int')) 实际分配了多个 int,不能简单判断为“只分配了 4 字节”。应修正元素类型和数量,并通过 try...finally 保证释放内存。Koffi 输出参数与内存分配说明
备注:内容仅供参考。
