Qt5 鼠标光标排查,这次让 Codex 走 TaoToken 查 setCursor
1. 现象复现setCursor 离开控件就失效Qt5 的 setCursor 看着简单出 bug 时能把人绕晕。TaoToken 官网上拿把 Key 就能把 Codex 接进来排查网址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 。先让 Codex 看懂原文那段 setCursor 代码button-setCursor(Qt::UpArrowCursor)之后注释说某些低层窗口实现会在光标离开部件后重置光标。于是常见场景来了——鼠标刚离开按钮箭头被打回 ArrowCursor想在窗口外显示 WaitCursor也经常一闪就被系统光标盖掉。这次排障我就用 Codex 走 TaoToken 查 setCursor把问题从「现象反复」压到「哪一行调用导致」。1.1 原文的按钮示例先落成一个可编译的最小工程原文里有一串成员初始化代码resize(300,200)、new QPushButton(AAA,this)、move(10,10)、resize(50,50)然后才是setCursor(Qt::UpArrowCursor)。直接复制进 Qt Widgets 工程其实有点碎我把它整理成最小可运行程序排障时也好让 Codex 对照行号#include QApplication #include QWidget #include QPushButton #include QCursor int main(int argc, char *argv[]) { QApplication app(argc, argv); QWidget w; w.resize(300, 200); QPushButton *button new QPushButton(AAA, w); button-move(10, 10); button-resize(50, 50); // 鼠标悬浮在按钮上时显示向上箭头 button-setCursor(Qt::UpArrowCursor); w.show(); return app.exec(); }这段代码本身跑起来按钮上能看到 UpArrowCursor。但只要鼠标在那个 50×50 的小方块上一滑光标就会在箭头和默认箭头之间“闪跳”。这里有个容易误判的细节Qt::UpArrowCursor本来就是箭头形状只是指向斜上方顶部还带一个小勾当你看到它“变回普通箭头”时先想一下是不是视觉差异再想是不是真的走了unsetCursor()。1.2 父部件光标和默认 ArrowCursor 的继承陷阱原文注释里写得很清楚若某部件没有调用setCursor或者调用了unsetCursor()它会使用父部件的光标默认值是Qt::ArrowCursor。这带来一个隐蔽问题子部件设置了光标但父部件没有设置鼠标在子部件和父部件之间移动时两者都是箭头系形状你很难察觉“是否被重置”。如果把按钮改成Qt::IBeamCursor文本编辑用的竖线现象立刻明显鼠标在按钮上是竖线滑出按钮到窗口空白区域就变回箭头。这其实是正确的 Qt 行为——按钮的setCursor只负责按钮自己的区域父窗口没有设置自然用ArrowCursor。真正的 bug 场景是鼠标还在按钮上光标却提前变回箭头或者离开按钮后原定的全局等待光标没有接上。后面这种才轮到QApplication::setOverrideCursor上场。2. setOverrideCursor 与 restoreOverrideCursor 的成对原则原文解释了QApplication::setOverrideCursor(Qt::WaitCursor)是“应用程序强制光标”会显示在所有窗口部件里直到restoreOverrideCursor()或另一个setOverrideCursor()被调用。这段的逻辑和部件级setCursor不是一个层级部件光标是“局部音量”override 光标是“系统总开关”。但总开关不是无限期的它必须被成对关掉否则整个程序会一直卡在等待光标里。2.1 为什么窗口外光标还是会被重置Linux 上的窗口管理器、Windows 的 Win32 光标处理都会在光标离开某个窗口客户区时按“当前命中的窗口/控件”重新设置系统光标形状。Qt 的QApplication::setOverrideCursor在 Qt 内部维护了一个 override 栈但某些低层平台实现并不会每帧都强行覆盖系统光标。这跟原文那句“即使捕获了鼠标某些低层窗口实现也会在光标离开部件后重置光标”完全对得上。所以你在mousePressEvent里调用setOverrideCursor(Qt::WaitCursor)鼠标按住不放拖到窗口外面光标可能恢复成普通箭头。这不是你的代码“没调用”而是平台层接管了。要让 override 持续生效通常要在mouseMoveEvent里反复检查并重新压栈或者改用QApplication::setOverrideCursor之后不调用restoreOverrideCursor直到鼠标事件真正结束。2.2 Codex 帮我校验的顺序到底谁最后动了光标我把上面那段最小示例和下面这版“带 override”的代码一起贴给 Codex并让它对照原文逐行说明调用顺序#include QApplication #include QWidget #include QPushButton #include QCursor int main(int argc, char *argv[]) { QApplication app(argc, argv); QWidget w; w.resize(300, 200); QPushButton *button new QPushButton(AAA, w); button-move(10, 10); button-resize(50, 50); button-setCursor(Qt::UpArrowCursor); // 按下按钮后强制转成等待光标 QObject::connect(button, QPushButton::pressed, [app]() { app.setOverrideCursor(Qt::WaitCursor); }); // 松开后立刻恢复 QObject::connect(button, QPushButton::released, [app]() { app.restoreOverrideCursor(); }); w.show(); return app.exec(); }Codex 指出了一个我容易漏掉的点pressed和released事件是成对出现的才算稳定如果用户在按钮上按下把鼠标拖出窗口外再松开released可能发不到按钮上restoreOverrideCursor就不会执行整个程序进入持续 WaitCursor 状态。正确做法是把restoreOverrideCursor放到QApplication的事件过滤或QWidget::mouseReleaseEvent中处理。这类“窗口外释放导致状态没回来”的问题光看setCursor那一行是找不到根因的得看绑定信号的对称性。3. 让 Codex 通过 TaoToken 对照原文代码段点检排障的第一步不是改代码而是把原文的那段 Qt5 光标代码完整丢给 Codex并明确指令“逐行告诉我 setCursor、unsetCursor、setOverrideCursor、restoreOverrideCursor 这四个函数的调用顺序以及哪一步会导致光标在窗口外失效。”Codex 拿到代码后会先画调用链再检查事件绑定最后指出 override 没有配对的问题。这一整套对话要走得稳模型通道不能断所以我先在 TaoToken 上创建了 API Key。3.1 在 TaoToken 拿到 Key再把 Codex 指到接口去 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 完成注册进入控制台创建一把 API Key复制下来当作YOUR_API_KEY。这里有个容易混淆的点官网落地页和接口地址是两回事。注册、创建 Key、看模型广场、看用量全走官网填进 Codex 的 Base URL 则是https://taotoken.net/api末尾不要加/v1。Codex 的配置文件在~/.codex/config.toml把默认模型指向 TaoToken 提供的模型 ID。模型 ID 不要自己编以模型广场当时列表为准配置示例如下# ~/.codex/config.toml model 以 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 模型广场显示的模型 ID 为准 model_provider taotoken [model_providers.taotoken] name TaoToken base_url https://taotoken.net/api env_key TAOTOKEN_API_KEY然后在当前 shell 里导出环境变量export TAOTOKEN_API_KEYYOUR_API_KEYCodex 启动后就会用taotoken这个 provider 去请求模型。以后要换模型只需要回到模型广场复制新的模型 ID改掉config.toml里的model这一行不需要再申请别家 Key也不需要切换一堆环境变量。3.2 Codex 回复里的关键检查点我让 Codex 针对原文给出了一份排障点清单这里直接列出对我最有用的三个setCursor只影响单个部件离开部件后光标由平台层决定是否重置要全局强制必须走QApplication::setOverrideCursor。setOverrideCursor是压栈操作多次调用会让 override 计数变深每调用一次就要匹配一次restoreOverrideCursor否则光标“锁死”在最后压入的形状。自定义QCursor构造时热区参数0,0是图片左上角默认参数-1,-1才是图片中心热区偏了用户点击时命中位和视觉位对不上。这三条正好对应原文代码里最容易踩的三个坑。以前我遇到光标不跟手会怀疑是不是窗口管理器的问题重装驱动甚至换系统主题。现在先让 Codex 对着原文这段代码分析调用顺序几分钟就能确认是不是 override 栈没配对。4. QCursor 自定义光标热区与 CursorShape 的配合原文中自定义光标的写法是QCursor my(QPixmap(a.jpg),0,0)注释里提到参数 2、参数 3 是光标感应点位置默认在图片中心。很多人在这一步图省事直接写QCursor my(QPixmap(a.jpg))结果鼠标热区落在图片中心视觉上总感觉“点不准”。这个问题在图形编辑器、画板类工具里特别明显你画一条线实际落笔点比光标箭头图标偏右下好几个像素。4.1 热区按用途选择0,0 表示左上角QCursor(const QPixmap pixmap, int hotX, int hotY)的热区坐标是相对图片左上角的偏移。0,0表示图片左上角作为感应点-1,-1表示让 Qt 自动计算图片中心。如果你用的是普通箭头素材通常希望热区在左上角附近这时写0,0是对的如果你用的是十字准星素材希望热区在中心最好显式写成QCursor(pixmap, pixmap.width()/2, pixmap.height()/2)不要依赖默认值因为素材尺寸一变默认中心点也跟着变。Codex 检查这段代码时还提醒我图片加载失败会导致QPixmap为空构造出来的QCursor无效setCursor之后看不到任何变化。所以生产环境要先做判空QPixmap cursorPixmap(a.jpg); if (cursorPixmap.isNull()) { button-setCursor(Qt::ArrowCursor); } else { QCursor my(cursorPixmap, 0, 0); button-setCursor(my); }4.2 Qt::CursorShape 枚举里够用的那几把光标原文最后提到Qt::CursorShape枚举类预定义了一批光标形状。排障时不用背全表但下面这几个和本文场景强相关的形状值得记住枚举值用途排障提示Qt::ArrowCursor标准箭头部件未设置光标时的默认值Qt::UpArrowCursor向上箭头原文示例用的形状和 ArrowCursor 视觉相近注意区分Qt::WaitCursor等待/忙碌配合setOverrideCursor做全局等待Qt::BlankCursor隐藏鼠标this-setCursor(Qt::BlankCursor)可隐藏指定控件光标Qt::IBeamCursor文本编辑竖线用来测试部件级光标是否生效特别明显Qt::CrossCursor十字准星配合热区中心点使用更直观Qt::BlankCursor是隐藏光标的快捷方式原文里的this-setCursor(Qt::BlankCursor)就是让整个窗口区域都看不见鼠标指针。这个在播放视频、全屏演示时很好用但如果忘记在鼠标事件里恢复用户会以为程序卡死。Codex 给的建议是隐藏光标时把光标位置记录下来再在mouseMoveEvent里用QCursor::setPos把它固定住避免鼠标消失后用户找不到方向。5. 排障清单光标被重置、等待不还原、热区偏移这次排障最终收敛出三类高频问题每类对应一个明确动作以后遇到可以直接照着查不用再逐行读 Qt 源码。5.1 光标离开按钮立刻变回箭头先查部件级setCursor是否写在show()之前或者有没有别的代码在enterEvent/leaveEvent里调用了unsetCursor()。如果都没有而且光标只在按钮外变回默认箭头这是正常行为。需要全局等待时不要依赖按钮自己的setCursor改用QApplication::setOverrideCursor。如果希望“离开按钮后仍是等待光标”可以在按钮的leaveEvent里再次调用setOverrideCursor保证 override 栈里始终有一个 WaitCursor。5.2 等待光标一直不消失几乎都是setOverrideCursor和restoreOverrideCursor没配对。Qt 内部维护 override 栈每次 set 都是压栈每次 restore 都是出栈。如果你在某处点击事件里 set 了两次但只 restore 一次光标会停在 WaitCursor。排查时可以用QApplication::overrideCursor()返回值判断是否还有激活的 overrideif (QApplication::overrideCursor()) QApplication::restoreOverrideCursor();放在mouseReleaseEvent或keyReleaseEvent里可以保证窗口外释放鼠标时也能兜底恢复。5.3 自定义光标热区对不上确认XCursor素材的尺寸再确认热区参数。小尺寸图标建议直接清空热区坐标让 Qt 默认取中心如果项目里统一要求左上角热区就把0,0写成宏或常量不要散落各处。验证方法很简单把a.jpg换成一张带明显左上角标记的图片比如左边 10px 涂红鼠标移动时看红色区域是否正好贴合系统点击点。若偏差正好是图片尺寸的一半就是“预期中心但写了左上角”的典型结果。6. 跑通之后去控制台对一下这次调用配置保存后先在 TaoToken 模型对话 里用同一把 Key 发一条测试消息确认模型 ID 和 Base URL 没填错。这条消息会被记进 TaoToken 的用量里之后回到 控制台 API Keys 能看到请求是否正常产生。如果打算长时间拿 Codex 写 Qt 排障代码可以顺便看下 Coding Plan 里的套餐是否够用Claude Code 和 Codex 的环境变量对照则可以在 接入文档 里找到完整参数。回看这次排障真正卡住我的并不是 Qt 的 API 文档而是“光标被平台层重置”这句话太容易被忽略。原文把它写在注释里一转眼就滑过去了。有了 Codex 帮忙逐行清理调用顺序再配合 TaoToken 统一的模型通道我不用在多个模型后台之间来回切换账号只用一个 Key 就把 setCursor、setOverrideCursor、restoreOverrideCursor 的调用链对齐了。下次再遇到类似的光标不跟手先到 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_end 把 Key 拿到然后让 Codex 对着代码块讲一遍事件顺序问题通常就显形了。