)
1. Qt Text Edit 控件程序化选择与修改的真实开发场景很多刚接触 Qt 桌面端开发的朋友第一次把QTextEdit拖到.ui文件里跑起来能打字、能复制粘贴就觉得这个控件已经会用了。可一旦产品经理提需求——点一下按钮把当前这一段文字标红、搜索框输入关键词自动选中所有匹配项、读取配置文件后高亮第 3 行——立刻就卡住了。鼠标选择谁都会但用代码去控制选择、定位、修改才是QTextEdit真正需要跨过去的那道坎。我自己在做一个日志查看器的时候就踩过这个坑。界面上有个QTextEdit显示日志旁边放了个标记错误行按钮需求是点击后把光标所在的那一整段背景变红。当时第一反应是去查QTextEdit有没有selectLine()之类的接口翻了半天文档没找到网上搜到的答案又都是零散的QTextCursor片段拼不起来。后来才明白Qt 的文本编辑体系里选择、定位、格式化这三件事全部由QTextCursor统一管理QTextEdit本身只是个显示容器真正干活的是光标对象。理解这一点之后整个思路就顺了。QTextEdit提供textCursor()拿到当前光标光标身上带着位置信息文本被组织成一个个QTextBlock块你可以粗略理解成段落一个块就是两次回车之间的一段文字块内部还能再细分到QTextFragment片段用来处理同一段里不同格式的文字。选择一段文字本质就是让光标从起点移动到终点移动过程中按住锚点Anchor这段区间就被选中了。这篇内容面向的是桌面端文本编辑功能开发我会把控件初始化、程序化选择、文本读写、格式化修改这几块拆开讲每一段都给可直接复制的代码。最后还会补上编译运行后的验证动作以及一个把模型能力接进编辑器的思路——用 TaoToken 的 API 给编辑器加个AI 润色选中文本的按钮让这个控件从能编辑变成能智能编辑。适合已经会写基础 Qt Widgets、但被光标和块绕晕的开发者。2. TaoToken 前置准备给编辑器接入模型能力在动手改控件之前先把外部能力这条线铺好。我们要做的是让QTextEdit里选中的文字能一键发给大模型做润色或改写再把结果写回控件。这一步需要三样东西一个可用的 API 地址、一个 API Key、一个模型 ID。TaoToken 在这里扮演的就是统一入口的角色官网在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 根地址是 https://taotoken.net/api 。先说清楚它是什么、能做什么、适合谁。TaoToken 提供的是兼容 OpenAI 风格的 HTTP 接口你用QNetworkAccessManager发一个 POST 请求带上Authorization: Bearer 你的Key就能拿到模型返回的文本。对 Qt 开发者来说好处是不用引入额外的 SDK标准网络模块就能搞定编译依赖干净。适合的场景就是这种桌面工具 轻量 AI 能力的组合比如文本润色、代码解释、翻译、摘要。拿 Key 的路径很直接打开控制台 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 登录后在 API Keys 页面创建一个新 Key复制出来保存好。这个 Key 只显示一次丢了就得重建。创建入口在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。模型 ID 这块我实测下来常用的几个都能直接调具体以文档里的模型列表为准文档地址 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。如果你只是想先在浏览器里试试模型通不通可以用模型对话页面 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 发一句话验证确认返回正常再写进代码。注意Key 不要硬编码进.cpp提交到仓库。建议放在QSettings或环境变量里读取后面配置片段我会给一个从环境变量取值的写法。如果你后续要做的是长期编码辅助、Agent 类工具而不是这种单次调用那更适合看 Coding Plan 方案 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 它面向的是持续性的编码场景。本篇聚焦的是编辑器控件本身模型调用只是锦上添花的那一步。3. 可复制的控件初始化与文本修改配置这一节是核心全部代码可以直接贴进你的工程。假设你在 Qt Designer 里已经放了一个QTextEditobjectName设为te_main另外放一个QPushButton叫btn_mark一个btn_polish。先看头文件里需要包含的东西#include QTextEdit #include QTextCursor #include QTextBlock #include QTextCharFormat #include QNetworkAccessManager #include QNetworkRequest #include QNetworkReply #include QJsonObject #include QJsonDocument #include QJsonArray #include QSettings控件初始化部分我习惯在构造函数里做几件事设置等宽字体方便看日志、开启撤销栈、给个初始文本方便测试。// 构造函数中 ui-setupUi(this); QFont mono(Consolas); mono.setPointSize(11); ui-te_main-setFont(mono); ui-te_main-setUndoRedoEnabled(true); ui-te_main-setPlainText( 第一行正常日志\n 第二行ERROR 连接超时\n 第三行正常日志\n 第四行ERROR 读取失败\n);接下来是程序化选择并格式化的完整函数。这是把 excerpt 里那段思路补全后的版本加了边界判断避免空块或越界崩溃void MainWindow::markCurrentBlockRed() { QTextCursor tc ui-te_main-textCursor(); QTextBlock blk tc.block(); if (!blk.isValid()) return; // 用块自身的 position 和 length 定位比 fragment 更稳 int start blk.position(); int end blk.position() blk.length() - 1; // 去掉块尾换行符 QTextCursor sel(ui-te_main-document()); sel.setPosition(start); sel.setPosition(end, QTextCursor::KeepAnchor); QTextCharFormat fmt; fmt.setForeground(Qt::red); fmt.setFontWeight(QFont::Bold); sel.mergeCharFormat(fmt); // merge 保留原有格式set 会覆盖 }这里有个细节值得说excerpt 里用的是blk.begin().fragment().position()这在大多数情况下能用但如果块内第一个片段是空的比如块刚被清空fragment()可能无效。直接用blk.position()和blk.length()更保险这也是我踩过坑之后改的写法。另外格式化用mergeCharFormat而不是setCharFormat前者是叠加后者是替换做标红这种增量操作时 merge 更符合直觉。再给一个按关键词选中所有匹配项的函数配合搜索框用void MainWindow::selectAllMatches(const QString keyword) { if (keyword.isEmpty()) return; QTextDocument *doc ui-te_main-document(); QTextCursor cursor(doc); cursor.beginEditBlock(); // 先清掉旧选择 QTextCursor clear(doc); clear.select(QTextCursor::Document); QTextCharFormat plain; plain.setBackground(Qt::transparent); clear.mergeCharFormat(plain); QTextCharFormat hl; hl.setBackground(QColor(255, 235, 59)); while (!cursor.isNull() !cursor.atEnd()) { cursor doc-find(keyword, cursor); if (cursor.isNull()) break; cursor.mergeCharFormat(hl); } cursor.endEditBlock(); }文本读写这块toPlainText()拿纯文本setPlainText()整体替换insertPlainText()在光标处插入。要注意setPlainText会清空撤销栈如果用户可能想撤销改用QTextCursor::insertText配合全选删除。关于模型调用的配置我用一个 JSON 结构来存方便从QSettings或环境变量读{ base_url: https://taotoken.net/api, api_key_env: TAOTOKEN_API_KEY, model_id: 你的模型ID, endpoint: /v1/chat/completions }对应的读取代码QString apiKey qEnvironmentVariable(TAOTOKEN_API_KEY); QString baseUrl https://taotoken.net/api; QString modelId 你的模型ID;提示base_url后面拼/v1/chat/completions就是完整的请求地址。如果你的工程用 CMake记得在CMakeLists.txt里加find_package(Qt6 COMPONENTS Network REQUIRED)并链接Qt6::Network否则QNetworkAccessManager会报未定义符号。4. 验证请求与编译运行后的成功结果代码写完得跑起来看结果。先说控件本身的验证再说模型请求的验证。控件验证很简单编译运行后把光标点到第二行点标记错误行按钮第二行应该整行变红加粗。再在搜索框输入ERROR点搜索两行 ERROR 应该都被黄色背景高亮。如果没反应先检查objectName是否和代码里一致这是最常见的低级错误。模型请求的验证我写一个独立的函数把选中文本发出去void MainWindow::polishSelection() { QTextCursor tc ui-te_main-textCursor(); if (!tc.hasSelection()) return; QString selected tc.selectedText(); QString apiKey qEnvironmentVariable(TAOTOKEN_API_KEY); if (apiKey.isEmpty()) { ui-te_main-append(未设置 TAOTOKEN_API_KEY); return; } QNetworkAccessManager *mgr new QNetworkAccessManager(this); QNetworkRequest req(QUrl(https://taotoken.net/api/v1/chat/completions)); req.setHeader(QNetworkRequest::ContentTypeHeader, application/json); req.setRawHeader(Authorization, (Bearer apiKey).toUtf8()); QJsonObject msg; msg[role] user; msg[content] 请润色以下文字保持原意只返回润色后的结果\n selected; QJsonArray messages; messages.append(msg); QJsonObject body; body[model] 你的模型ID; body[messages] messages; QNetworkReply *reply mgr-post(req, QJsonDocument(body).toJson()); connect(reply, QNetworkReply::finished, this, []() { QByteArray data reply-readAll(); QJsonObject obj QJsonDocument::fromJson(data).object(); QJsonArray choices obj[choices].toArray(); if (choices.isEmpty()) { ui-te_main-append(返回异常 QString::fromUtf8(data)); reply-deleteLater(); return; } QString result choices[0].toObject()[message] .toObject()[content].toString(); QTextCursor cur ui-te_main-textCursor(); cur.insertText(result); reply-deleteLater(); }); }编译运行后选中一段文字点AI 润色稍等一两秒选中内容会被替换成润色后的版本。如果控制台打印出返回异常把打印的原始 JSON 贴出来看通常是 Key 或模型 ID 的问题。成功的结果长这样选中第二行ERROR 连接超时点润色返回类似第二行错误——连接超时插入到光标位置。整个过程不需要重启程序网络请求是异步的界面不会卡。注意QNetworkAccessManager每次 new 一个不是最佳实践生产代码里应该作为成员变量复用。这里为了片段独立可复制才这么写。5. 本篇常见报错排查这一节按真实会遇到的报错来对每个都给定位思路。401 Unauthorized。返回体里通常是{error:{message:invalid api key}}。原因就三个Key 没设置、Key 复制时带了空格、Key 已失效。先在终端echo $TAOTOKEN_API_KEY确认环境变量有值再检查Authorization头是不是Bearer后面直接跟 Key中间只有一个空格。我见过有人写成Bearer: xxx多了个冒号直接 401。local proxy failed / connection refused。这个报错说明请求根本没发出去卡在本地网络层。检查你的QNetworkRequest地址是不是写成了https://taotoken.net/api少了/v1/chat/completions或者端口写错。另外确认工程链接了Qt6::Network没链接的话编译期就报错了不会到运行期。如果公司网络有出口限制确认taotoken.net在允许列表里。reading choices 时崩溃或返回空。典型表现是obj[choices].toArray()拿到空数组然后choices[0]越界。根因是返回体不是预期的 chat 格式可能是错误响应。正确做法是先判断obj.contains(error)有错误就打印obj[error]别直接取 choices。上面代码里我已经加了choices.isEmpty()的判断照着写就不会崩。OAuth / 认证相关报错。如果你用的是某些需要 OAuth 流程的工具链报错里会出现OAuth token expired之类。TaoToken 的 API Key 方式是静态 Bearer不涉及 OAuth 刷新遇到这类报错说明你混用了别的认证方式回到 API Keys 页面重新生成一个静态 Key 即可。光标定位不准选中的不是想要的那一段。这通常是块和片段理解偏差导致的。blk.position()是块在文档中的绝对偏移blk.length()包含块尾的换行符所以算 end 时要减 1。如果你用fragment().position()在块内有多种格式时会拿到第一个片段的位置不一定等于块起点。统一用blk.position()最稳。格式化后撤销失效。如果你在修改格式时没有用beginEditBlock()/endEditBlock()包起来多次 merge 会产生多个撤销步骤用户按一次 CtrlZ 只回退一步。批量操作时记得包起来让它成为一个原子操作。CC Switch / Cline MCP / Codex auth.json 场景。如果你是在这些工具里配置模型接入三件套必须写全Base URL 填https://taotoken.net/apiKey 填你生成的 API KeyModel ID 填文档里列出的模型标识。少任何一个都会连接失败。以auth.json为例结构大致是{ base_url: https://taotoken.net/api, api_key: 你的Key, model: 你的模型ID }字段名以对应工具的实际要求为准但三个值的来源是固定的。6. 把编辑器控件用顺手的几个实用建议写到这里控件本身的选择、修改、格式化、模型接入都跑通了。最后分享几个我实际用下来觉得省事的点。第一QTextCursor是可以脱离QTextEdit单独存在的只要你传入document()。这意味着你可以在后台线程里构造光标做文本分析只要不碰 UI 就行。做大批量文本处理时这个特性很有用。第二格式化尽量用mergeCharFormat需要重置为默认时再构造一个空QTextCharFormat去 merge。setCharFormat会把字体、颜色、背景全部替换掉很容易把用户原本的格式冲没。第三如果你要做的是代码编辑器而不是普通文本编辑QTextEdit够用但不够好语法高亮要自己写QSyntaxHighlighter。但如果你只是要一个能显示、能选、能改、能接 AI 的文本框QTextEdit加QTextCursor这套组合完全够别过度设计。第四模型调用记得做超时和错误兜底。QNetworkReply默认没有超时网络不好时会一直挂着。可以接一个QTimer比如 15 秒没返回就reply-abort()给用户一个请求超时的提示体验会好很多。需要查模型列表和接口细节时文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 想先在网页里试模型效果用 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite Key 的管理在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。把这几步串起来你的QTextEdit就不只是一个输入框而是一个能理解文本、能帮你改文本的编辑器控件了。