☰
Qt开发技术:Qt富文本(二)Qt文本光标操作、文档布局、富文本编辑、处理和Demo
2026/10/2 6:25:17 网站建设 项目流程

1. Qt 富文本编辑器里 QTextCursor 光标定位与选区处理到底怎么用

如果你正在做桌面端富文本编辑器,大概率绕不开 Qt 的 QTextDocument 和 QTextCursor。简单说,QTextDocument 负责“文档里有什么”,QTextCursor 负责“我要改哪里、选哪段”。它俩配合起来,就能实现加粗、改色、插表格、插图片、查找替换这些操作。适合谁?适合已经会用 QTextEdit 显示 HTML,但一到“精确选中第 3 行第 2 个词并改样式”就卡住的开发者。

我先把核心概念讲清楚。QTextDocument 内部把内容组织成一棵元素树:根框架 QTextFrame 下面挂着文本块 QTextBlock,块里是文本片段,块之间还能插表格 QTextTable、图片、列表。光标 QTextCursor 本质上是一个“位置指针”,它记录当前在文档字符流中的偏移量,同时可以带一个锚点 anchor,锚点到当前位置之间的内容就是选区。

关键点在于:Qt 把“结构”也编码进了字符流。也就是说,插入一个块、一个表格,都会消耗字符位置。所以你用 movePosition 移动光标时,移动的是“文档位置”,不是“可见字符数”。这一点不理解,后面选区就会错位。

举个最直观的例子。你从编辑器拿光标:

QTextEdit *editor = new QTextEdit(); QTextCursor cursor(editor->textCursor());

或者直接从文档构造:

QTextDocument *document = editor->document(); QTextCursor cursor(document);

新构造的光标默认在文档开头,也就是第一个空块的位置。此时你 insertText,文字就写进第一个块。

选区怎么做?靠 MoveMode。默认是 MoveAnchor,移动光标会丢掉原来的选区;用 KeepAnchor,移动时保留锚点,等于按住 Shift 选择:

cursor.movePosition(QTextCursor::StartOfWord); cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);

上面这段就是选中光标所在的整个单词。QTextCursor 提供了大量 MoveOperation:Left、Right、Up、Down、StartOfLine、EndOfLine、StartOfBlock、EndOfBlock、NextBlock、PreviousBlock、WordRight、WordLeft 等等。做富文本编辑器,这些枚举你得熟,因为几乎所有“选中某段再改格式”的逻辑都是“先 movePosition 定位,再 KeepAnchor 选中,最后 mergeCharFormat 或 setCharFormat”。

这里有个容易踩的坑:mergeCharFormat 是“合并格式”,只改你设置的属性,其他属性保留;setCharFormat 是“整体替换”,没设置的属性会被重置。做“只加粗不改颜色”这种需求,必须用 mergeCharFormat。

再讲分组操作。用户按一次撤销,应该撤销一整段逻辑,而不是一个字一个字撤。这时候用 beginEditBlock / endEditBlock 把一组操作包起来:

cursor.beginEditBlock(); cursor.movePosition(QTextCursor::StartOfWord); cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor); cursor.mergeCharFormat(boldFormat); cursor.endEditBlock();

这样撤销栈里只记一条。但注意别把太多操作塞进一个 block,否则用户想撤销中间某一步就做不到了,粒度要合理。

多个光标同时编辑同一文档是允许的,QTextDocument 会保证写入不冲突,但 QTextEdit 只显示一个闪烁光标。所以如果你在后台用另一个 cursor 改了文档,想让用户看到结果,得把光标设回去:editor->setTextCursor(cursor)。

理解到这一层,你就能明白:富文本编辑器的本质,就是“用光标在文档字符流上做增删改 + 用格式对象描述样式”。下一节我们先解决模型能力接入的问题,再回到可复制的配置和 Demo。

2. TaoToken 统一 Key 与 API 通道接入模型能力的前置准备

做富文本编辑器,很多时候你会想加“AI 润色”“AI 续写”“AI 排版”这类功能。这时候就需要一个稳定的模型调用通道。TaoToken 提供统一 Key 和 API 通道,把模型对话、编码计划、控制台、API Keys 这些入口集中管理,省得你在多个平台之间来回切换。

先说清楚它是什么、能做什么、适合谁。TaoToken 是一个模型能力接入平台,你可以用同一个 Key 调用不同模型,适合个人开发者、小团队做原型验证,也适合把模型能力嵌进桌面应用里。对 Qt 开发者来说,最实用的场景就是:在 QTextEdit 里选中一段文字,点个按钮,调用模型做润色或翻译,再把结果写回文档。

前置准备分三步。第一步,拿到 API Key。访问官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,注册后在控制台创建 Key。控制台地址是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,API Keys 管理页是 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。建议给每个应用单独建一个 Key,方便排查和吊销。

第二步,确认 Base URL。API 入口是 https://taotoken.net/api ,注意这个地址不带 UTM 参数,直接用于代码里的请求地址。文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,接入前先扫一眼,确认当前支持的模型 ID 和请求格式。

第三步,选模型。如果你只是做文本润色、摘要,用通用对话模型就够;如果你要做长期编码或 Agent 类任务,可以看 Coding Plan,地址是 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。想先试试模型效果,可以直接用模型对话页 https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite 。

这里要强调一个原则:Key 不要硬编码进源码,更不要提交到 Git。Qt 项目里可以用 QSettings 存本地配置,或者用环境变量读取。下面给一个读取环境变量的写法:

QString apiKey = qEnvironmentVariable("TAOTOKEN_API_KEY"); if (apiKey.isEmpty()) { qWarning() << "TAOTOKEN_API_KEY not set"; }

如果你用 Claude Code 做辅助开发,Anthropic 兼容入口在 https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claude-code-anthropic&utm_campaign=rewrite ,配置时同样需要 Base URL、Key、Model ID 三件套。

前置准备做完,你手里应该有三样东西:一个可用的 Key、Base URL https://taotoken.net/api 、一个确定的 Model ID。下一节我们把它们写进可复制的配置片段,并和 Qt 富文本编辑器的光标操作结合起来。

3. 可复制的 QTextCursor 配置与 TaoToken 接入配置片段

这一节给你可以直接抄的代码。先讲 Qt 侧的光标与文档布局配置,再讲 TaoToken 的接入配置。两部分都会给完整片段,路径和字段名保持一致。

先看富文本编辑器的初始化。创建一个 QTextEdit,设置文档默认字体和块格式:

QTextEdit *editor = new QTextEdit(this); QTextDocument *doc = editor->document(); doc->setDefaultFont(QFont("Microsoft YaHei", 12)); QTextBlockFormat blockFormat; blockFormat.setLineHeight(150, QTextBlockFormat::ProportionalHeight); blockFormat.setTopMargin(6); blockFormat.setBottomMargin(6); QTextCursor cursor(doc); cursor.select(QTextCursor::Document); cursor.setBlockFormat(blockFormat); cursor.clearSelection();

上面这段把整篇文档的行高设成 150%,段间距上下各 6 像素。setLineHeight 的第一个参数是数值,第二个参数是类型,ProportionalHeight 表示按比例。文档布局相关的还有 QTextFrameFormat,用来控制框架的边距、填充、边框:

QTextFrameFormat frameFormat; frameFormat.setMargin(32); frameFormat.setPadding(8); frameFormat.setBorder(4); frameFormat.setBorderBrush(QBrush(Qt::darkGray)); QTextFrame *rootFrame = doc->rootFrame(); rootFrame->setFrameFormat(frameFormat);

根框架设置好,整篇文档就有了统一的外边距和内边距。注意 setMargin 是框架外部边距,setPadding 是内部填充,setBorder 是边框宽度,三者叠加才是最终视觉间距。

接下来是光标选区的核心操作。假设你要实现“选中当前行并加粗”:

void boldCurrentLine(QTextEdit *editor) { QTextCursor cursor = editor->textCursor(); cursor.beginEditBlock(); cursor.movePosition(QTextCursor::StartOfBlock); cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); QTextCharFormat fmt; fmt.setFontWeight(QFont::Bold); cursor.mergeCharFormat(fmt); cursor.endEditBlock(); editor->setTextCursor(cursor); }

这段代码里,StartOfBlock 和 EndOfBlock 是块级定位,比 StartOfLine 更稳,因为软换行不会影响块边界。mergeCharFormat 保证只改字重,不动颜色和字号。

再给一个“查找并高亮所有匹配词”的片段:

void highlightAll(QTextEdit *editor, const QString &keyword) { QTextDocument *doc = editor->document(); QTextCursor cursor(doc); QTextCharFormat fmt; fmt.setBackground(QColor("#fff3a0")); cursor.beginEditBlock(); while (!cursor.isNull() && !cursor.atEnd()) { cursor = doc->find(keyword, cursor); if (!cursor.isNull()) { cursor.mergeCharFormat(fmt); } } cursor.endEditBlock(); }

doc->find 返回的光标已经选中了匹配文本,直接 mergeCharFormat 即可。注意循环条件里 cursor.atEnd() 的判断,避免死循环。

现在讲 TaoToken 接入配置。如果你用 Cline MCP 或类似工具,配置通常是 JSON 格式。下面给一个通用片段,字段名按常见约定:

{ "provider": "taotoken", "baseUrl": "https://taotoken.net/api", "apiKey": "${TAOTOKEN_API_KEY}", "model": "your-model-id", "timeout": 60000 }

如果你用 Codex 的 auth.json 风格配置,结构类似:

{ "auths": { "taotoken": { "base_url": "https://taotoken.net/api", "api_key": "${TAOTOKEN_API_KEY}", "model_id": "your-model-id" } } }

三件套必须齐全:Base URL 是 https://taotoken.net/api ,Key 从环境变量或本地配置读,Model ID 按文档里当前可用的填。缺任何一个都会报错。

在 Qt 里调用时,用 QNetworkAccessManager 发 POST 请求:

QNetworkRequest req(QUrl("https://taotoken.net/api/chat/completions")); req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); req.setRawHeader("Authorization", ("Bearer " + apiKey).toUtf8()); QJsonObject body; body["model"] = modelId; QJsonArray messages; QJsonObject msg; msg["role"] = "user"; msg["content"] = selectedText; messages.append(msg); body["messages"] = messages; QNetworkReply *reply = manager->post(req, QJsonDocument(body).toJson());

selectedText 就是从 QTextCursor 选区里取出来的文本:

QString selectedText = editor->textCursor().selectedText();

注意 selectedText 里段落分隔符是 U+2029,不是 \n,发给模型前最好替换一下:

selectedText.replace(QChar(0x2029), '\n');

配置片段给完了,下一节我们实际发一次请求,验证结果能不能正确写回文档。

4. 验证请求与成功结果:从选区到模型返回再写回文档

这一节做端到端验证。目标是:在 QTextEdit 里选中一段文字,调用 TaoToken 的模型接口,拿到润色结果,替换回原选区。整个过程要能看到成功结果,也要能定位失败点。

先写一个完整的验证函数。假设你已经有了 apiKey、modelId,并且 editor 是当前编辑器:

void polishSelection(QTextEdit *editor, const QString &apiKey, const QString &modelId) { QTextCursor cursor = editor->textCursor(); if (!cursor.hasSelection()) { qWarning() << "no selection"; return; } QString text = cursor.selectedText(); text.replace(QChar(0x2029), '\n'); QNetworkAccessManager *manager = new QNetworkAccessManager(editor); QNetworkRequest req(QUrl("https://taotoken.net/api/chat/completions")); req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); req.setRawHeader("Authorization", ("Bearer " + apiKey).toUtf8()); QJsonObject body; body["model"] = modelId; QJsonArray messages; QJsonObject msg; msg["role"] = "user"; msg["content"] = QString("请润色以下文字,保持原意,只返回润色后的内容:\n") + text; messages.append(msg); body["messages"] = messages; QNetworkReply *reply = manager->post(req, QJsonDocument(body).toJson()); QObject::connect(reply, &QNetworkReply::finished, [=]() { if (reply->error() != QNetworkReply::NoError) { qWarning() << "request failed:" << reply->errorString(); reply->deleteLater(); return; } QByteArray data = reply->readAll(); QJsonDocument doc = QJsonDocument::fromJson(data); QJsonObject obj = doc.object(); QJsonArray choices = obj["choices"].toArray(); if (choices.isEmpty()) { qWarning() << "empty choices"; reply->deleteLater(); return; } QString result = choices[0].toObject()["message"].toObject()["content"].toString(); QTextCursor writeCursor = editor->textCursor(); writeCursor.beginEditBlock(); writeCursor.insertText(result); writeCursor.endEditBlock(); editor->setTextCursor(writeCursor); reply->deleteLater(); }); }

这段代码的关键点有几个。第一,请求地址是 https://taotoken.net/api/chat/completions ,Base URL 是 https://taotoken.net/api ,路径拼上去。第二,Authorization 头是 Bearer 加 Key。第三,返回结构里 choices 是数组,取第一个的 message.content。第四,写回时用 insertText 替换选区,因为光标本身有选区,insertText 会覆盖选中内容。

成功结果长什么样?你在编辑器里选中“这是一段测试文字”,点按钮,几秒后选区被替换成润色后的版本,比如“这是一段用于测试的文本”。同时撤销栈里只多了一条记录,按 Ctrl+Z 能一次性还原。

如果你想先验证模型通道是否通,不用写 Qt 代码,直接用 curl:

curl -X POST https://taotoken.net/api/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TAOTOKEN_API_KEY" \ -d '{ "model": "your-model-id", "messages": [{"role": "user", "content": "你好"}] }'

返回 JSON 里有 choices 数组就说明通道正常。这一步能排除掉大部分配置问题。

验证时还要注意文档布局的影响。如果你在写回前改过块格式,insertText 会继承当前光标位置的字符格式。想让结果用默认格式,可以在写回前先 setCharFormat 重置:

QTextCharFormat plain; writeCursor.setCharFormat(plain);

另外,如果选区跨多个块,insertText 会把它们合并成一个块。想保留块结构,得按块遍历处理。这个在下一节排错时会细说。

验证通过后,你就有了一个可用的“AI 润色”功能。接下来把常见错误集中排查一遍。

5. 本篇常见错误排查:401、local proxy failed、reading choices、OAuth

这一节按真实报错来。你在接入和调试过程中,大概率会遇到下面几类问题。我按错误信息分类,给出原因和解决路径。

第一类,401 Unauthorized。报错通常是{"error":{"message":"invalid api key"}}或直接 401。原因有三个:Key 没设置、Key 拼错、Authorization 头格式不对。检查顺序是:先确认环境变量 TAOTOKEN_API_KEY 有值,再确认代码里读的是同一个变量,最后确认头是Bearer加 Key,Bearer 后面有一个空格。如果你把 Key 写进了 JSON 配置,确认没有多余引号或换行。API Keys 管理页在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite ,可以去那里重新生成一个对比测试。

第二类,local proxy failed。这个报错通常出现在你本地配了代理,但代理没启动或端口不对。Qt 的 QNetworkAccessManager 默认会读系统代理设置。如果你不需要代理,显式关掉:

QNetworkProxyFactory::setUseSystemConfiguration(false);

或者在请求上设置 NoProxy:

QNetworkProxy proxy; proxy.setType(QNetworkProxy::NoProxy); manager->setProxy(proxy);

注意,这里说的是本地网络配置问题,不是让你去用什么特殊工具,只是把系统代理干扰排除掉。

第三类,reading choices 相关报错。典型信息是Cannot read property 'choices' of undefined或choices is empty。原因是返回的 JSON 结构和你预期的不一样。可能情况:请求体里 model 字段填错,服务端返回了错误对象而不是正常响应;或者返回是流式格式,你按非流式解析。排查方法是先把原始返回打出来:

qDebug() << "raw response:" << data;

看清楚顶层有没有 choices。如果没有,看有没有 error 字段。如果是流式,需要按data:前缀逐行解析。

第四类,OAuth 相关报错。如果你用 Claude Code 或类似工具,报错可能是OAuth token expired或invalid_grant。这类问题通常出在认证方式上。用 API Key 接入时,不需要走 OAuth 流程,直接配 Base URL、Key、Model ID 三件套即可。Anthropic 兼容入口在 https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claude-code-anthropic&utm_campaign=rewrite ,配置时确认字段名和文档一致。如果工具同时支持 OAuth 和 API Key,优先选 API Key,少一层刷新逻辑。

除了这四类,还有几个 Qt 侧的高频坑。一是 selectedText 里的 U+2029 没替换,发给模型后格式乱掉。二是 insertText 写回时没开 edit block,撤销栈碎成很多条。三是跨块选区用 insertText 合并了块结构,用户会发现段落没了。解决办法是按块处理:

QTextCursor c = editor->textCursor(); int start = c.selectionStart(); int end = c.selectionEnd(); c.setPosition(start); while (c.position() < end) { c.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); // 对当前块做处理 c.movePosition(QTextCursor::NextBlock); }

四是文档特别大时界面卡顿。Qt 对小块处理更好,可以按固定间隔插入换行,或者用 maximumBlockCount 限制块数量。批量插入时用 beginEditBlock 包起来,能明显减少重绘次数。

排错的核心思路是:先确认通道通(curl 能返回),再确认 Qt 侧请求构造对(打印原始返回),最后确认写回逻辑对(检查光标位置和格式)。按这个顺序,大部分问题都能定位。

6. 把模型能力接进 Qt 富文本编辑器的下一步

到这里,你已经有了可复制的 QTextCursor 操作片段、QTextDocument 布局配置、完整的请求验证代码,以及一份排错清单。接下来怎么走,取决于你的场景。

如果你只是做文本润色、翻译、摘要这类单次调用,用模型对话页先试效果最省事:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=chat&utm_campaign=rewrite 。确认 prompt 和返回格式满意了,再搬进 Qt 代码。

如果你要做长期编码辅助,或者把 Agent 能力嵌进编辑器做自动排版、自动生成表格,可以看 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。它更适合持续性的任务,不用每次单独配。

接入过程中遇到认证或请求格式问题,先查文档:https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。Key 的管理和轮换在 API Keys 页:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 。需要看整体入口就去官网:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 。

最后给一个实用建议:把“选区文本提取、请求发送、结果写回”封装成一个独立的类,比如 AiTextHelper,输入 QTextCursor,输出处理后的文本。这样你的富文本编辑器核心逻辑和模型调用解耦,换模型或换通道时只改一个地方。光标操作那部分保持纯 Qt,不掺网络逻辑,调试起来会轻松很多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询