1. 从一次编辑器状态栏翻车说起在 QT 桌面编辑器里做状态栏最容易翻车的一个小需求就是实时显示光标当前所在的行号。看起来只是QTextEdit里取个数字但真写起来很多人第一次都会踩到「行号不刷新」「换行后差一行」「多段落时数字乱跳」这几个坑。我自己在做一个 Markdown 草稿工具时就因为在cursorPositionChanged信号里写错了取值方式导致状态栏永远停在 1调了半天才发现是blockNumber()和显示行号之间差了一个偏移。这篇就围绕「QT QTextEdit 获取光标所在行的行号」这个具体场景把可复制的代码、settings.json配置骨架以及如何用 TaoToken 统一 Key 把 AI 辅助编码接进你的 QT 工程一次讲清楚。适合正在写 QT 文本编辑器、笔记软件、代码片段管理器的同学也适合想把 AI 补全能力接进桌面端但不想每个模型都单独配 Key 的开发者。核心检索词就三个QTextEdit、光标、行号读完你能直接跑起来。先说结论QTextEdit里取行号靠的是QTextCursor::blockNumber()它返回的是从 0 开始的段落块编号所以显示给用户时要1。真正让行号「跟着光标动」的关键是把取值逻辑挂到cursorPositionChanged()信号上而不是只在按钮点击时算一次。下面按「问题场景 → TaoToken 前置 → 可复制配置 → 验证 → 排障 → 接入」的顺序展开。2. 原问题与场景QTextEdit 行号到底怎么算2.1 blockNumber 是段落块不是视觉行QTextEdit的文档模型是QTextDocument它把内容组织成一个个 block段落块。QTextCursor::blockNumber()返回的就是光标所在 block 的索引从 0 开始。这里有个关键区别block 是按换行符切分的段落不是按控件宽度折行后的视觉行。也就是说如果你一段文字很长、在界面上被自动折成三行blockNumber()仍然只算一个 block。QTextCursor tc ui-textEdit-textCursor(); int rowNum tc.blockNumber() 1; // 显示给用户的行号上面这两行就是最核心的取值。1是因为用户认知里的行号从 1 开始而blockNumber()从 0 开始。如果你要的是「视觉行号」考虑自动折行那得用QTextLayout去算复杂度高很多绝大多数编辑器状态栏用的都是 block 行号够用。2.2 为什么行号不刷新新手最常见的写法是在某个按钮的槽函数里算一次行号然后设到 label 上。这样只有点按钮才更新光标移动时状态栏纹丝不动。正确做法是连接信号connect(ui-textEdit, QTextEdit::cursorPositionChanged, this, MainWindow::updateCursorLine);然后在updateCursorLine()里取blockNumber()并刷新 UI。这样每次光标位置变化包括键盘移动、鼠标点击、输入字符都会触发行号自然实时变化。2.3 多段落场景下的边界当文档里有多个段落、空行、或者用户全选时blockNumber()返回的是光标「起点」所在的 block。如果你需要选区跨行的信息得配合selectionStart()和selectionEnd()分别取 block 再算范围。日常状态栏显示单行号用blockNumber()就够了。3. TaoToken 前置统一 Key 与 API 通道3.1 为什么桌面编辑器要接统一 Key做 QT 编辑器时AI 辅助功能通常包括补全当前行、解释选中代码、生成注释。如果每个模型都单独申请 Key、单独写请求逻辑工程里会散落一堆 endpoint 和鉴权代码。TaoToken 提供的是统一 Key 和统一 API 通道你只需要在settings.json里配一次代码里走同一个 base URL切换模型只改一个字段。官网入口在这里https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基址是 https://taotoken.net/api 这个不加 UTM。注意TaoToken 是合规的 API 聚合通道不是什么灰色中转你按正常 HTTP 客户端调用即可。3.2 拿 Key 与看文档先到控制台创建 API Keyhttps://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。创建后复制那串 Key待会填进settings.json。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 里面有各语言的最小请求示例QT 里用QNetworkAccessManager发 POST 就行。如果你只是想先验证模型通不通可以用模型对话页快速试一句https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。长期做编码辅助、Agent 类功能建议看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。4. 可复制配置settings.json 骨架与行号代码4.1 settings.json 配置骨架下面这份骨架可以直接放进你的 QT 工程资源目录或用户配置目录字段含义我写在注释里JSON 不支持注释实际使用时删掉注释行或改用_comment字段。{ ai: { provider: taotoken, baseUrl: https://taotoken.net/api, apiKey: sk-你的Key填这里, model: claude-3-5-sonnet, timeoutMs: 30000, maxTokens: 1024 }, editor: { showLineNumber: true, lineNumberOffset: 1, updateOnCursorMove: true } }baseUrl固定用https://taotoken.net/api不要带 UTM 参数那是给网页跳转用的。apiKey建议不要硬编码进仓库实际项目里从环境变量或系统钥匙串读取这里为了演示先写明文。lineNumberOffset就是前面说的1偏移抽成配置方便以后改。4.2 读取配置的 QT 代码用QJsonDocument解析即可#include QFile #include QJsonDocument #include QJsonObject struct AiConfig { QString baseUrl; QString apiKey; QString model; int timeoutMs 30000; }; AiConfig loadConfig(const QString path) { AiConfig cfg; QFile f(path); if (!f.open(QIODevice::ReadOnly)) return cfg; auto doc QJsonDocument::fromJson(f.readAll()); auto ai doc.object().value(ai).toObject(); cfg.baseUrl ai.value(baseUrl).toString(); cfg.apiKey ai.value(apiKey).toString(); cfg.model ai.value(model).toString(); cfg.timeoutMs ai.value(timeoutMs).toInt(30000); return cfg; }4.3 行号获取与状态栏刷新把行号逻辑单独抽成一个槽函数方便复用void MainWindow::updateCursorLine() { QTextCursor tc ui-textEdit-textCursor(); int rowNum tc.blockNumber() 1; // 光标所在行号 int colNum tc.positionInBlock() 1; // 列号顺手加上 ui-statusBar-showMessage( QString(行 %1, 列 %2).arg(rowNum).arg(colNum)); }连接信号connect(ui-textEdit, QTextEdit::cursorPositionChanged, this, MainWindow::updateCursorLine);如果你还想在 AI 补全时知道「当前行内容」可以这样取QTextCursor tc ui-textEdit-textCursor(); QString currentLine tc.block().text(); // 当前 block 的纯文本tc.block().text()拿到的是光标所在段落的文本配合行号一起发给 AI就能实现「解释这一行」这类功能。5. 验证请求打印 blockNumber 确认实时变化5.1 最小验证动作先不接 AI只验证行号。在updateCursorLine()里加一行qDebug()void MainWindow::updateCursorLine() { QTextCursor tc ui-textEdit-textCursor(); int rowNum tc.blockNumber() 1; qDebug() blockNumber: tc.blockNumber() displayRow: rowNum; ui-statusBar-showMessage(QString(行 %1).arg(rowNum)); }运行程序在QTextEdit里敲几行字然后用方向键上下移动光标。你会在应用输出里看到blockNumber从 0、1、2 递增状态栏同步显示 1、2、3。鼠标点到不同行数字也会立刻跳。这一步过了说明行号逻辑没问题。5.2 验证 AI 通道行号通了之后验证 TaoToken 通道。用QNetworkAccessManager发一个最小请求void MainWindow::testAiChannel() { AiConfig cfg loadConfig(:/config/settings.json); QNetworkRequest req(QUrl(cfg.baseUrl /v1/chat/completions)); req.setHeader(QNetworkRequest::ContentTypeHeader, application/json); req.setRawHeader(Authorization, (Bearer cfg.apiKey).toUtf8()); QJsonObject body; body[model] cfg.model; QJsonArray msgs; QJsonObject m; m[role] user; m[content] 只回复两个字通了; msgs.append(m); body[messages] msgs; auto *reply manager-post(req, QJsonDocument(body).toJson()); connect(reply, QNetworkReply::finished, this, [reply]() { qDebug() reply-readAll(); reply-deleteLater(); }); }如果返回里能看到模型回复说明 Key、baseUrl、模型名三者都对。返回 401 就是 Key 错404 多半是路径拼错超时则是网络或timeoutMs设置问题。5.3 把行号上下文发给 AI验证通过后把行号和当前行内容拼进 promptQString prompt QString(当前第 %1 行内容%2\n请解释这行代码) .arg(rowNum).arg(currentLine);这样 AI 辅助就是「带上下文」的比只发一句「解释代码」准确得多。6. 本篇常见错排查6.1 行号永远显示 1九成是没连cursorPositionChanged信号只在初始化时算了一次。检查connect是否写在了构造函数里、对象指针是否为空。另一个可能是你在updateCursorLine()里用了缓存的QTextCursor成员变量而不是每次重新textCursor()取。记住每次都要重新取。6.2 换行后行号差一如果你显示的是blockNumber()没1用户看到的第一行就是 0。检查lineNumberOffset配置有没有生效。还有一种情况是你用了QTextBlock::blockNumber()但文档开头有隐藏 block这种少见打印出来对比即可。6.3 多段落时数字乱跳blockNumber()按段落算如果你的文档里一段被自动折行成多行视觉上行号会「跳号」。这是预期行为不是 bug。要视觉行号得自己用QTextLayout逐行算成本高一般编辑器不做。6.4 AI 请求 401 / 超时401 先查 Key 有没有多余空格、有没有带Bearer前缀。超时先看baseUrl是不是写成了带 UTM 的网页地址必须是https://taotoken.net/api。再确认timeoutMs别设太小30 秒比较稳。6.5 settings.json 读不到QT 资源文件路径要用:/前缀用户目录文件要用绝对路径。QFile::open失败时先打印f.errorString()多数是路径写错或文件没加进.qrc。7. 接入收尾把 Key 和行号串起来行号逻辑和 AI 通道都验证过之后剩下的就是组装。我的做法是cursorPositionChanged触发时除了刷新状态栏还把「行号 当前行文本」缓存到一个成员变量里用户触发 AI 补全时直接读缓存拼 prompt避免每次都重新取 cursor。这样既实时又省事。Key 管理上建议把settings.json里的apiKey换成从环境变量读取代码里用qgetenv(TAOTOKEN_API_KEY)仓库里只留占位符。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 里面有更完整的请求参数说明Key 在 https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 创建想先跑通模型再写代码用 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 试一句最快。长期做编码辅助和 AgentCoding Plan 在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。最后留一个我踩过的坑blockNumber()在空文档里返回 01后显示 1这是对的别以为空文档该显示 0。状态栏从 1 开始用户才不困惑。