Win32 里把窗口类的 wc.hCursor 写成 NULL,本意是放弃类光标,改在 WndProc 的 WM_SETCURSOR 里按 LOWORD(lParam) 判断 HTCLIENT 还是 HTCAPTION,再调 SetCursor 切到 IDC_CURSOR1 或 IDC_CURSOR2。可实际跑起来,鼠标滑进客户区,光标还是系统默认箭头,IDC_CURSOR2 像从来没被加载过。这类问题只盯 MSDN 上 LoadCursor、SetCursor 的函数声明很难定位,因为返回值语义、DefWindowProc 的兜底处理、resource.h 与 .rc 的 ID 对应、.cur 文件里的 HotSpot,任意一个环节偏一点,表现都是“光标不换”。本文用 TaoToken 给 Codex 准备好可用的 Key 与 Base URL(官网:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ),把 resource.h、WndProc 里 WM_SETCURSOR 的分支、注册窗口类的流程一起交给它做静态核对,把“为什么客户区没按 IDC_CURSOR2 切换”拆成可验证的几条。需要说明的是,TaoToken 在这里只提供 Key 与 Base URL,不参与 Win32 的光标加载与绘制,光标最终是否切换仍由你的资源 ID、消息返回值和运行结果决定。
一、Win32 光标不换的现场:wc.hCursor=NULL 与 WM_SETCURSOR 的分工
先把原笔记里的关键流程摆清楚,否则后面贴给 Codex 的代码片段会缺上下文。
资源侧,光标资源和图标资源类似,都是先添加资源,再由代码按 ID 或名字加载:
HCURSOR LoadCursor(HINSTANCE hInstance, LPCTSTR lpCursorName);hInstance 可以传 NULL,此时取系统预定义光标,比如 IDC_ARROW、IDC_IBEAM。传自己的实例句柄时,lpCursorName 既可以是字符串名,也可以是 MAKEINTRESOURCE 包出来的资源 ID。每个 .cur 文件内部还带一个 HotSpot,也就是“热区”,点击时以这个点为准,它和资源 ID 是两回事:ID 决定 LoadCursor 取哪张图,HotSpot 决定这张图在哪个像素点上生效。
窗口类侧,注册窗口类时可以给 wc.hCursor 赋一个类光标:
wc.hCursor = LoadCursor(hIns, MAKEINTRESOURCE(IDC_CURSOR1));类光标的作用范围是“默认情况”。当鼠标位于客户区、窗口没有自己处理 WM_SETCURSOR 时,DefWindowProc 会拿类光标来设置。把这个字段置成 NULL,等于告诉系统“我这里没有默认光标”,此时如果 WndProc 里没有把 WM_SETCURSOR 处理干净,系统就会用默认箭头兜底。
WndProc 侧,WM_SETCURSOR 的两个参数要拆开看:
- wParam 是当前光标句柄;
- lParam 的低字 LOWORD(lParam) 是命中测试码,HTCLIENT 表示客户区,HTCAPTION 表示标题栏,还有 HTLEFT、HTBOTTOMRIGHT 等边框区域;
- lParam 的高字 HIWORD(lParam) 是当前鼠标消息 ID。
原笔记里典型写法是:
case WM_SETCURSOR: { HCURSOR hCur = LoadCursor(g_hInstance, (char*)IDC_CURSOR2); if (LOWORD(lParam) == HTCLIENT) { SetCursor(hCur); return 0; } } break;这段代码有几个地方会在运行时出偏差:LoadCursor 的第二个参数在 Unicode 构建下用(char*)IDC_CURSOR2强转并不规范;判断只覆盖 HTCLIENT,其他命中码直接 break 给 DefWindowProc,而类光标已经被置 NULL;最关键的是 SetCursor 之后return 0,返回值语义与“已处理”不一致。现象就是:分支明明进了,SetCursor 也调了,光标却没按 IDC_CURSOR2 显示。
二、TaoToken 前置:给 Codex 准备 Key 与 Base URL
这一步只做两件事:拿到 Key,把 Codex 的请求地址指到 TaoToken。不涉及 Win32 的任何资源加载逻辑。
- 打开 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,按页面提示注册账号。
- 进入控制台的 API Keys 页面创建 Key,复制出来,下面统一写成
YOUR_API_KEY。这个 Key 只放在本机环境变量或 Codex 配置里,不要写进 .rc、resource.h 或提交到代码仓库。 - 记住 Base URL:
https://taotoken.net/api。注意两点,不带/v1,不加任何 UTM 参数。Codex 的 config.toml 里填错这两处,症状通常是请求直接失败,而不是模型回答变差。
如果你在团队里统一给 Codex 配接入,建议把 Base URL 和 Key 的读取方式固定在文档里,避免每个人各写一份。控制台里的 API Keys 页面和接入文档是这一步的主要参考入口,排障时先回头确认这两个值,比在 Win32 代码里反复猜要快。
三、可复制配置:config.toml、resource.h 与 WndProc 片段
Codex 的配置放在~/.codex/config.toml,Windows 下一般是%USERPROFILE%\.codex\config.toml。下面是一份可复制的骨架,模型 ID 按你在 TaoToken 侧实际可用的写:
model = "MODEL_ID" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY" wire_api = "responses"Key 用环境变量传入。Linux/macOS:
export TAOTOKEN_API_KEY=YOUR_API_KEYWindows PowerShell:
$env:TAOTOKEN_API_KEY="YOUR_API_KEY"resource.h 里两个光标 ID 必须是唯一的、且能被 .rc 引用到,例如:
#define IDC_CURSOR1 2001 #define IDC_CURSOR2 2002对应的 .rc 里,资源类型写 CURSOR,文件名与实际磁盘上的 .cur 一致:
IDC_CURSOR1 CURSOR "cursor1.cur" IDC_CURSOR2 CURSOR "cursor2.cur"WndProc 里可以按下面的结构核对,注意 WM_SETCURSOR 的返回值和类光标的配合:
case WM_SETCURSOR: { UINT hit = LOWORD(lParam); if (hit == HTCLIENT) { HCURSOR hCur = LoadCursor(g_hInstance, MAKEINTRESOURCE(IDC_CURSOR2)); if (hCur != NULL) { SetCursor(hCur); return TRUE; // 表示本条消息已处理,不再交给 DefWindowProc 兜底 } } break; // 非客户区交回默认处理,前提是类光标有合理值 }如果希望标题栏、边框等非客户区用 IDC_CURSOR1,注册窗口类时不要置 NULL,而是:
wc.hCursor = LoadCursor(hIns, MAKEINTRESOURCE(IDC_CURSOR1));然后在 HTCLIENT 分支里覆盖成 IDC_CURSOR2。这样即使 WM_SETCURSOR 的某个分支没覆盖到,窗口也还有一个可用的类光标,不会直接退化成默认箭头。
把下面这段提示词连同你的代码片段一起交给 Codex,让它做核对而不是自由发挥:
我在写 Win32 窗口程序,wc.hCursor 置为 NULL,想在 WM_SETCURSOR 里按 LOWORD(lParam)==HTCLIENT 把客户区光标切换到 IDC_CURSOR2,但运行时没有切换。 请只依据我贴出的 resource.h、.rc 片段、WndProc 和注册窗口类代码,核对: 1) WM_SETCURSOR 的返回值语义,SetCursor 之后 return 0 与 return TRUE 的差别; 2) LoadCursor(g_hInstance, (char*)IDC_CURSOR2) 在 Unicode 构建下是否规范, 是否应该改成 MAKEINTRESOURCE; 3) wc.hCursor=NULL 时 DefWindowProc 对 HTCLIENT 与 HTCAPTION 的处理路径; 4) resource.h 中的 IDC_CURSOR1/IDC_CURSOR2 与 .rc 中两条 CURSOR 记录的 ID 是否一一对应,HotSpot 是否与预期点击位置一致。 输出最小修改点和需要我手动验证的步骤,不要改动无关代码。Codex 在这里的定位是“对着代码逐条核对”,结论仍然要你编译运行来确认。
四、验证请求与运行结果:客户区光标是否按 IDC_CURSOR2 切换
先验证 Codex 侧请求确实发出去了。最直接的方式是在终端里用一段极短的对话确认配置生效,或者打开模型对话页面选择同一个模型发一条消息,能正常返回就说明 Key 与 Base URL 这条链路通了。如果这里报鉴权失败或地址错误,先回到 config.toml 检查base_url是否被误写成https://taotoken.net/api/v1或带上了 UTM 参数,再检查环境变量名与env_key是否一致。
再看 Win32 侧的验证,按下面的顺序做,不要一上来就怀疑资源文件:
- 在 WM_SETCURSOR 的 HTCLIENT 分支里临时加一句输出,确认分支确实被执行,比如把命中码、LoadCursor 的返回值打印到调试输出。
- 确认 LoadCursor 返回值不是 NULL。如果返回 NULL,问题在资源侧,与 SetCursor 的调用顺序无关。
- 把返回值从
return 0改成return TRUE,重新编译运行,鼠标移进客户区观察是否按 IDC_CURSOR2 切换。 - 鼠标移到标题栏,观察是否按 IDC_CURSOR1 显示;如果这里也异常,说明类光标或非客户区分支还有问题。
- 如果光标能换但点击位置偏移,去检查 .cur 文件里的 HotSpot,必要时用光标编辑器调整,或者改用 CreateCursor 显式指定 xHotSpot、yHotSpot。
只有“分支执行了、LoadCursor 返回非 NULL、返回值语义正确、运行中光标确实切换”这四条同时成立,才能说明这段 WM_SETCURSOR 的逻辑跑通了。
五、本篇常见错排查:WM_SETCURSOR 返回值、LoadCursor、HotSpot、资源 ID
按出现频率排一遍。
第一,WM_SETCURSOR 里 SetCursor 之后return 0。这个返回值会被当成“没有处理”,最终仍可能走到 DefWindowProc 的兜底逻辑,把刚设置的光标覆盖掉。把 HTCLIENT 分支改为return TRUE,是与“已处理”语义一致的写法。这一点在只读函数声明时最容易忽略,因为它不报错,只影响运行时表现。
第二,LoadCursor(g_hInstance, (char*)IDC_CURSOR2)的强转。typedef 之后 LPCTSTR 在 Unicode 构建下是const wchar_t*,用(char*)转换整数资源 ID 在不同字符集下行为不一致,规范写法是MAKEINTRESOURCE(IDC_CURSOR2)或显式调用MAKEINTRESOURCEW。如果这里编译能过但资源取不到,LoadCursor 会返回 NULL,后续 SetCursor(NULL) 并不会把光标切成 IDC_CURSOR2。
第三,resource.h 与 .rc 的 ID 对不上。#define IDC_CURSOR2 2002和.rc里的IDC_CURSOR2 CURSOR "cursor2.cur"必须指向同一个资源记录。常见情况是 .rc 里写的是另一个 ID 或字符串名字,而代码用整数 ID 去取,取不到就返回 NULL。让 Codex 把两份文件放在一起比对,比人眼来回翻要稳。
第四,HotSpot 与资源 ID 混淆。ID 决定加载哪张光标图,HotSpot 决定这张图的热区位置。HotSpot 写偏了不会导致“光标不换”,但会出现点击点与光标视觉中心错位,看起来像是“换错了光标”。如果 ID 对得上、LoadCursor 返回值正常、返回值也改成了 TRUE,但仍觉得不对劲,就把检查重点放到 .cur 文件内部的热点上。
第五,wc.hCursor=NULL 之后没有给非客户区兜底。命中码不是 HTCLIENT 时直接 break,类光标又是 NULL,系统只能用默认箭头。更稳的做法是保留一个类光标作为默认,只在 HTCLIENT 分支覆盖;或者把所有关心的命中码都显式处理,并在处理时返回 TRUE。
第六,资源文件没有编进工程。修改 .rc 或新增 .cur 之后没有重新编译资源,LoadCursor 仍会按旧资源返回。这种情况用调试输出看 LoadCursor 返回值最直接。
把以上几条整理成清单后,再让 Codex 逐条对照你的代码,通常能定位到具体是哪一行导致客户区光标不按 IDC_CURSOR2 切换。
六、需要继续排障或长期用 Codex:走 API Keys、接入文档与 Coding Plan
如果你现在就要把 Key 和 Base URL 配起来继续排查这次光标问题,直接进 API Keys 页面创建或复制 Key(https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite ),配置字段与路径细节对照接入文档(https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite )。config.toml 里的base_url仍然只填https://taotoken.net/api,不带/v1、不加 UTM。
如果你只是想先确认模型侧是否连通,不急着动 Win32 工程,可以打开模型对话页面发一条消息验证(https://taotoken.net/model-chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite )。返回正常,就说明 Key、Base URL 这条链路没问题,剩下的精力全部放回 resource.h、.rc、WM_SETCURSOR 和 HotSpot 上。
如果你接下来还要长期用 Codex 查类似 Win32 消息与资源的问题,比如字符串资源、加速键资源、WM_COMMAND 的 HIWORD 判定这些,可以看一下 Coding Plan(https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ),把配置一次性固定下来,省去每次重新填 Key 和地址的步骤。回到本篇的结论:光标本该切换却没换,优先核对 WM_SETCURSOR 的返回值语义、LoadCursor 的资源 ID 与取值方式、以及 wc.hCursor=NULL 之后非客户区有没有兜底;这三处确认完,再去看 .cur 的 HotSpot 和资源文件的编译状态。