1. 现象复现:setCursor 离开控件就失效
Qt5 的 setCursor 看着简单,出 bug 时能把人绕晕。TaoToken 官网上拿把 Key 就能把 Codex 接进来排查,网址是 https://taotoken.net/?utm_source=taotoken_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_source=taotoken_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_source=taotoken_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_KEY=YOUR_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()返回值判断是否还有激活的 override:
if (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_source=taotoken_aicg_blog_end 把 Key 拿到,然后让 Codex 对着代码块讲一遍事件顺序,问题通常就显形了。