PAYATHON 2026

为什么 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 更合适。

修正后仍然找不到,按什么顺序检查?

  1. 先检查标题条件。使用修正后的同一声明,分别调用:

    console.log('按类名:', FindWindowW('Progman', null));
    console.log(
      '按类名和标题:',
      FindWindowW('Progman', 'Program Manager')
    );

    如果前者成功、后者失败,就说明 "Program Manager" 这个标题条件没有匹配。

  2. 用 Spy++ 的窗口树或搜索功能,按类名 Progman 查找,核对实际标题和句柄。也可以通过 EnumWindows 枚举顶层窗口,再用 GetClassNameW、GetWindowTextW 查看属性。FindWindowW 不搜索子窗口。Spy++ 窗口查看说明、FindWindowW 查找范围

  3. 检查 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 输出参数与内存分配说明

备注:内容仅供参考。