1. 从零搭建 QT 文本编辑器:QTextEdit 与 QTextCursor 到底能做什么
如果你刚开始自学 QT 桌面开发,想做一个带格式的文本编辑器,QTextEdit 基本是绕不开的控件。它不只是个能打字的框,底层其实是一套完整的富文本文档模型:QTextDocument 管文档,QTextBlock 管段落,QTextFragment 管片段,QTextFrame 管框架,而 QTextCursor 就是你在文档里移动、选中、插入的那支“笔”。这套模型能做什么?简单说,段落背景色、行高、首行缩进、超链接、浮动框架、表格,全都能用代码精确控制。适合谁?适合已经会一点 QT 信号槽、想深入富文本编辑的初学者,也适合需要做日志高亮、代码片段展示、简易排版工具的开发者。
我试过直接往 QTextEdit 里塞 HTML,确实快,但一旦要做“选中某段改颜色”“遍历所有段落找关键词”“在光标处插入带格式的块”,HTML 字符串就不够用了,必须拿到 QTextCursor。这篇笔记就围绕 QTextEdit、QTextCursor、QTextBlock、QTextFrame 展开,把光标定位、块遍历、框架嵌套这几件事拆成可复制的代码。你跟着敲一遍,编译运行后能看到段落背景、行高、缩进、超链接、左右浮动框架的实际效果,也能在 qDebug 里看到遍历结果。
先明确一个关系:QTextEdit 是部件,QTextDocument 是它内部的文档对象,QTextCursor 是操作文档的游标。纯文本里一行就是一个段落,也就是一个 QTextBlock;富文本里一个段落可以被切成多个 QTextFragment,因为不同字符格式会被拆开。QTextFrame 则是更外层的容器,文档有一个根框架,框架里可以再放块或子框架。理解这四层,后面写代码就不会迷路。
2. 前置准备:TaoToken 接入与 QT 工程环境
这一节先把两件事说清楚:一是 QT 工程怎么建、头文件怎么加;二是如果你想让编辑器后续接入模型能力做智能补全或文本润色,怎么用 TaoToken 拿到可用的 API 配置。TaoToken 官网是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 地址是 https://taotoken.net/api ,注意 API 地址后面不加 UTM 参数。它的作用是把模型调用统一成 OpenAI 兼容接口,你可以在 QT 里用 QNetworkAccessManager 发请求,也可以先在模型对话页面验证返回格式。
QT 工程这边,新建一个 Widget 应用,在 widget.h 里加上这些头文件:
#include <QTextEdit> #include <QTextCursor> #include <QTextBlockFormat> #include <QTextCharFormat> #include <QTextFragment> #include <QTextFrame> #include <QTextDocumentFragment> #include <QDebug> #include <QFontMetrics>widget.cpp 的构造函数里,先 resize 窗口,再 new 一个 QTextEdit,设置默认字体。这里有个小坑:QTextEdit 默认字体可能偏小,建议用 QTextDocument::setDefaultFont 统一设置,后面 QFontMetrics 算缩进也方便。
resize(900, 700); QTextEdit *editor = new QTextEdit(this); editor->resize(850, 650); QTextDocument *doc = editor->document(); QFont myfont("Bahnschrift", 14); doc->setDefaultFont(myfont); QTextCursor cursor(editor->textCursor());如果你要接 TaoToken 做文本处理,先在控制台创建 API Key,地址是 https://taotoken.net/console ,Key 管理在 https://taotoken.net/api-keys 。拿到 Key 后,Base URL 填 https://taotoken.net/api ,Model ID 按你选的模型填,比如 claude 系列或 gpt 系列。这三件套——Base URL、Key、Model ID——在 QT 里发请求时一个都不能少。想先验证模型是否通,可以直接用模型对话页面 https://taotoken.net/models 发一条测试消息,确认返回正常再写进代码。
对于长期做编码或 Agent 的场景,可以看 Coding Plan https://taotoken.net/coding-plan ,它更适合持续调用。Claude Code 相关接入文档在 https://taotoken.net/doc ,里面有 Anthropic 兼容的配置说明。注意,TaoToken 只是提供 API 接入,不是让你绕过任何本地环境,QT 工程本身还是跑在你自己的机器上。
3. 可复制配置:QTextCursor 插入块、片段与框架
这一节给出一份可以直接粘贴进 widget.cpp 的配置片段,覆盖 QTextBlockFormat、QTextCharFormat、QTextFrameFormat 三种格式对象。先看段落格式。QTextBlockFormat 管段落背景、行高、边距、首行缩进、缩进层级。下面这段代码先设置紫色背景和行高,再插入文本块:
QTextBlockFormat bf1; bf1.setBackground(QColor(190, 150, 200)); bf1.setLineHeight(120, 1); bf1.setBottomMargin(50); cursor.setBlockFormat(bf1); cursor.insertBlock(); cursor.insertText("setBlockFormat bf1 紫色背景段落");注意 setLineHeight 的第二个参数,只有一行时用 1 或 2 效果不同,官方枚举在 QTextBlockFormat::LineHeightTypes 里查。接着是字符格式 QTextCharFormat,管字体、上划线、下划线、单词间距、垂直对齐。下面这段设置上划线、字体、字号、单词间距,并让字符相对基线往下对齐:
QTextCharFormat cf1; QFont qf1; qf1.setOverline(true); qf1.setFamily("Agency FB"); qf1.setPointSize(26); cf1.setFont(qf1); cf1.setFontWordSpacing(10); cf1.setVerticalAlignment(QTextCharFormat::AlignSubScript); cursor.insertBlock(bf1, cf1); cursor.insertText("insertBlock(bf1, cf1) 同时应用段落和字符格式");框架格式 QTextFrameFormat 用来做浮动容器。下面这段插入一个绿色左浮动框架,宽度 45%,再插入一个棕色右浮动框架。关键点:插入第一个框架后,光标在框架内部,直接插第二个框架会变成嵌套。要变成平等关系,必须把光标重置到根框架的 lastCursorPosition:
QTextFrameFormat fmt; fmt.setBackground(Qt::green); fmt.setMargin(1); fmt.setBorder(1); fmt.setPadding(2); fmt.setWidth(QTextLength(QTextLength::PercentageLength, 45)); fmt.setPosition(QTextFrameFormat::FloatLeft); cursor.insertFrame(fmt); cursor.insertBlock(); cursor.insertText("左浮动框架内容"); QTextFrame *myroot = doc->rootFrame(); cursor = myroot->lastCursorPosition(); QTextFrameFormat fmt2; fmt2.setBackground(QColor(180, 142, 120)); fmt2.setMargin(1); fmt2.setBorder(3); fmt2.setPadding(2); fmt2.setWidth(QTextLength(QTextLength::PercentageLength, 45)); fmt2.setPosition(QTextFrameFormat::FloatRight); cursor.insertFrame(fmt2); cursor.insertBlock(); cursor.insertText("右浮动框架内容");如果你要把这些配置写成 JSON 或 TOML 方便外部读取,可以这样组织。JSON 版本:
{ "block": { "background": "#be96c8", "lineHeight": 120, "bottomMargin": 50 }, "char": { "family": "Agency FB", "pointSize": 26, "overline": true, "wordSpacing": 10 }, "frame": { "background": "#00ff00", "widthPercent": 45, "position": "FloatLeft" } }TOML 版本:
[block] background = "#be96c8" lineHeight = 120 bottomMargin = 50 [char] family = "Agency FB" pointSize = 26 overline = true wordSpacing = 10 [frame] background = "#00ff00" widthPercent = 45 position = "FloatLeft"这些配置读进来后,分别 set 到 QTextBlockFormat、QTextCharFormat、QTextFrameFormat 即可。注意 QTextCharFormat 的 setVerticalAlignment 会影响字号显示,实测下来 AlignSubScript 会让字变小,如果只是想要上划线,可以不加这一行。
4. 验证请求与成功结果:遍历块、片段与框架
写完插入代码,怎么确认真的生效?最直接的办法是遍历文档结构,把 QTextBlock、QTextFragment、QTextFrame 都打印出来。先看 QTextBlock 遍历。QTextDocument 的 begin() 返回第一个块,next() 往后走,isValid() 判断是否有效:
QTextBlock currentBlock = doc->begin(); while (currentBlock.isValid()) { qDebug() << "block text:" << currentBlock.text(); currentBlock = currentBlock.next(); }再看 QTextFragment 遍历。一个块内可能有多个片段,因为字符格式不同会被拆开。下面这段遍历当前光标所在块的所有片段,判断是否是超链接:
QTextBlock::iterator it; for (it = cursor.block().begin(); !(it.atEnd()); ++it) { QTextFragment frag = it.fragment(); if (frag.isValid()) { if (frag.charFormat().isAnchor()) { qDebug() << "find anchor" << frag.text() << frag.charFormat().anchorHref(); } else { qDebug() << "未找到超链接" << frag.text(); } } }实测输出类似这样:未找到超链接 "first sentence1111111",未找到超链接 "second sentence2222...",find anchor "this is to baidu Anchor" "http://www.baidu.com"。这说明超链接片段被正确识别,anchorHref 也能取到。
最后看 QTextFrame 遍历。根框架的 begin() 到 end() 之间,可能遇到块,也可能遇到子框架。下面这段从根框架开始,遇到块打印文本,遇到框架就再进一层:
QTextFrame *myroot = doc->rootFrame(); for (QTextFrame::iterator it = myroot->begin(); !(it.atEnd()); ++it) { QTextBlock curBlock = it.currentBlock(); QTextFrame *curFrame = it.currentFrame(); if (curBlock.isValid() && !curBlock.text().isEmpty()) { qDebug() << "根框架内部文本块" << curBlock.text(); } else if (curFrame) { qDebug() << "find a frame"; for (QTextFrame::iterator it2 = curFrame->begin(); !(it2.atEnd()); ++it2) { QTextBlock grandBlock = it2.currentBlock(); QTextFrame *grandFrame = it2.currentFrame(); if (grandBlock.isValid() && !grandBlock.text().isEmpty()) { qDebug() << "子框架内文本块" << grandBlock.text(); } else if (grandFrame) { qDebug() << "子框架内还有框架"; } } } }成功结果会打印出根框架内的文本块、两个浮动框架以及它们内部的子块。如果你发现第二个框架跑到了第一个框架里面,检查是否漏了cursor = myroot->lastCursorPosition();这一行。这个坑我踩过,插入框架后光标默认在框架内,不重置就会嵌套。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth
这一节把 QT 富文本开发和 TaoToken 接入时容易遇到的报错对照一遍。先说 QT 侧。第一个常见错是遍历不到片段。如果你在 insertText 之后又 insertBlock,当前块的迭代器就找不到别的块里的 fragment,因为迭代器只作用于当前块。解决办法是每次遍历前重新获取 cursor.block(),或者用 QTextDocument 的块遍历再进片段遍历。
第二个错是框架嵌套错乱。表现是第二个框架显示在第一个框架内部,而不是并排。原因就是插入框架后光标位置在框架内,必须用doc->rootFrame()->lastCursorPosition()重置。如果你用的是子框架,也可以用frame->lastCursorPosition()。
第三个错是 setLineHeight 不生效。只有一行文本时,第二个参数必须用 2,用 1 可能显示不出来。这个在官方文档里写得比较隐晦,实测下来确实如此。
再说 TaoToken 接入侧。401 通常表示 API Key 无效或没带上。检查请求头里 Authorization 是否为Bearer <你的Key>,Key 是否从 https://taotoken.net/api-keys 正确复制。local proxy failed 一般是本地网络配置问题,检查 Base URL 是否写成 https://taotoken.net/api ,不要多加路径或 UTM 参数。reading choices 报错通常出现在流式返回解析时,返回体里 choices 字段为空,检查 Model ID 是否填对,以及请求体里 stream 参数和解析逻辑是否匹配。OAuth 相关报错多见于 Claude Code 接入场景,参考 https://taotoken.net/doc 里的 Anthropic 配置说明,确认认证方式选对。
如果你在 QT 里用 QNetworkAccessManager 发请求,建议先把返回的 JSON 完整打印出来,确认结构再解析。模型对话页面 https://taotoken.net/models 可以帮你快速验证 Key 和 Model ID 是否可用,不用每次都编译 QT 工程。
6. 语义一致 CTA:从文本编辑器到模型接入
到这里,QTextEdit、QTextCursor、QTextBlock、QTextFrame 的核心操作基本走了一遍。你可以用这些代码做出段落高亮、超链接识别、浮动框架排版,也能遍历文档结构做关键词统计或格式检查。下一步如果想让编辑器具备智能能力,比如选中文本后调用模型润色、根据上下文补全代码,就需要接入 API。
接入时优先看 API Keys 和接入文档:API Keys 在 https://taotoken.net/api-keys ,接入文档在 https://taotoken.net/doc 。想先验证模型返回格式,用模型对话 https://taotoken.net/models 。如果是长期编码或 Agent 场景,Coding Plan 在 https://taotoken.net/coding-plan 。Claude Code 相关配置参考 https://taotoken.net/doc 里的 Anthropic 部分。
最后留一个实用技巧:在 QT 里做文本处理时,尽量把 QTextCursor 的操作包在 beginEditBlock 和 endEditBlock 之间,这样撤销栈会合并成一步,用户体验更好。遍历文档时,QTextDocument::iterator 只能找到块,QTextFrame::iterator 能找到块和框架,QTextBlock::iterator 只能找到片段,三者用途不同,别混用。把这些关系理清,后面做更复杂的富文本功能会顺很多。