
1. Qt 笔记模块文本编辑与增改功能开发从 QTextEdit 到信号槽联动的完整落地Qt 笔记模块的文本编辑与增改功能说白了就是让一个 QTextEdit 既能打字、又能改格式还能把标题和富文本内容存进表格、点一下就能改回来。适合正在做 Qt 课设或桌面笔记工具的同学尤其是卡在“富文本怎么存”“选中文本后按钮状态怎么同步”这两个点上的人。我试过把加粗、倾斜、下划线、字号、颜色、插图全部塞进一个 selectionChanged 槽里结果代码又长又难维护后来拆成独立函数才清爽。这篇就按“能跑通”的标准把 CMake 配置、QTextEdit 关键代码、QSqlTableModel 增改数据流、以及用 TaoToken 统一 Key 接入 AI 辅助生成与审查代码的链路一次讲清。目标很明确一天内跑通可运行的笔记增改闭环边界用例也能覆盖到。先明确场景。笔记模块和普通增删改查最大的区别在于内容不是纯文本而是富文本。QTextEdit 支持 HTML 子集字体、字号、颜色、加粗、倾斜、下划线、图片都能塞进去。但一旦涉及“选中一段文字改格式”就必须用 QTextCursor 捕获选区再调 mergeCharFormat。如果只改光标之后的内容就用 mergeCurrentCharFormat。这两个 API 是整篇的核心搞懂它们后面所有格式按钮都是同一套模板。增改功能的数据流分两条线。第一条是 UI 线表格只显示标题点“新建”跳到第二页输入标题和正文点“保存”把标题插回表格。第二条是存储线正文富文本要转成 HTML 字符串存进数据库修改时再读出来 setHtml 回填。很多教程只讲 UI 不讲存储结果保存完一重启内容就没了。这篇会把 QSqlTableModel 的 setData 和 submitAll 也带上保证闭环。AI 辅助编码这块我用 TaoToken 统一 Key 来打通。原因很简单写 Qt 槽函数时经常要查“QTextCursor 怎么遍历选区”“QSqlTableModel 怎么提交修改”与其来回翻文档不如把报错和需求直接丢给模型让它生成可编译的片段再自己审查。TaoToken 的 API 通道兼容常见调用方式一个 Key 就能覆盖生成和审查两个环节省去多平台切换的麻烦。下面从环境配置开始一步步落地。2. TaoToken 前置统一 Key 与 API 通道接入 AI 辅助编码链路在写 Qt 代码之前先把 AI 辅助通道搭好后面遇到报错或需要生成槽函数模板时直接调用不用中途停下来折腾配置。TaoToken 的作用是提供一个统一的 API 入口你只需要一个 Key就能在编码过程中让模型帮你生成 QTextCursor 操作片段、审查信号槽连接是否正确、或者解释 QSqlTableModel 的提交行为。官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 注意 API 地址不带 UTM 参数。第一步拿到 Key。进入控制台页面 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 登录后在 API Keys 区域创建一个新 Key。建议按项目命名比如 qt-note-dev方便后面区分。创建后复制保存页面只显示一次。如果你还没决定用哪个模型可以先到模型对话页 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 试一下生成效果确认能正常返回再写进配置。第二步确定接入方式。Qt 项目里调用 AI 有两种常见做法一种是在终端用 curl 快速验证另一种是写进项目的配置文件供脚本调用。我建议先用 curl 验证 Key 是否可用命令如下curl -X POST https://taotoken.net/api/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer 你的Key \ -d { model: claude-sonnet-4-20250514, messages: [ {role: user, content: 用一句话说明 QTextCursor::mergeCharFormat 的作用} ] }如果返回里有 choices 字段和正常文本说明通道通了。这一步很关键因为后面所有 AI 辅助都依赖这个通道。如果返回 401先检查 Key 是否复制完整、Bearer 后面有没有多余空格。第三步把配置写进项目。Qt 项目通常用 CMake 管理我习惯在项目根目录放一个.env或ai_config.json把 Base URL、Key、Model ID 三件套写清楚。这样无论是终端脚本还是 IDE 插件都能读同一份配置。示例 JSON 如下{ base_url: https://taotoken.net/api, api_key: sk-你的Key, model_id: claude-sonnet-4-20250514, timeout: 60 }注意 base_url 只写到 /api不要带 /v1具体路径在请求时拼接。model_id 根据你在模型对话页选定的模型填写。timeout 设 60 秒生成代码片段足够用。第四步验证配置能被读取。写一个简单的 shell 脚本读 JSON 并请求#!/bin/bash API_KEY$(python3 -c import json;print(json.load(open(ai_config.json))[api_key])) BASE_URL$(python3 -c import json;print(json.load(open(ai_config.json))[base_url])) MODEL_ID$(python3 -c import json;print(json.load(open(ai_config.json))[model_id])) curl -s -X POST $BASE_URL/v1/chat/completions \ -H Content-Type: application/json \ -H Authorization: Bearer $API_KEY \ -d {\model\:\$MODEL_ID\,\messages\:[{\role\:\user\,\content\:\输出一个 QTextEdit 加粗槽函数模板\}]}跑通后你就有了一个随时可用的 AI 辅助通道。后面写 selectionChanged 遍历逻辑时如果记不清 QTextCursor 的 movePosition 参数直接把问题丢进去让它生成片段你再审查。这样编码效率会明显提升而且不用在多个平台之间切换 Key。如果你打算长期做 Qt 开发、经常需要 AI 辅助生成和审查代码可以考虑 Coding Plan 页面 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 了解更稳定的调用方案。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有完整的参数说明和错误码对照遇到 401 或超时可以先去查。3. 可复制配置CMake、QTextEdit 与 QSqlTableModel 关键代码片段这一节直接给可复制的配置和代码。先看 CMakeLists.txtQt6 和 Qt5 写法略有差异下面以 Qt6 为例关键是要把 Widgets、Sql 两个模块链进去cmake_minimum_required(VERSION 3.16) project(TickTask LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC ON) find_package(Qt6 REQUIRED COMPONENTS Widgets Sql) add_executable(TickTask main.cpp note.cpp note.h note.ui ) target_link_libraries(TickTask PRIVATE Qt6::Widgets Qt6::Sql )如果你用的是 Qt5把Qt6换成Qt5find_package的 COMPONENTS 写法不变。AUTOMOC 和 AUTOUIC 必须开否则 ui 文件和 Q_OBJECT 宏不会自动处理。接下来是 QTextEdit 的核心格式操作。加粗、倾斜、下划线三个按钮共用同一套模板区别只在 setFontWeight、setFontItalic、setFontUnderline。以加粗为例void Note::on_boldButton_clicked() { QTextCursor cursor ui-noteTextEdit-textCursor(); QTextCharFormat format; format.setFontWeight(ui-boldButton-isChecked() ? QFont::Bold : QFont::Normal); if (cursor.hasSelection()) { cursor.mergeCharFormat(format); ui-noteTextEdit-setTextCursor(cursor); } else { ui-noteTextEdit-mergeCurrentCharFormat(format); } }这段代码的关键判断是 hasSelection。有选区就改选区没选区就改后续输入格式。mergeCharFormat 不会覆盖其他格式属性只改你设置的那一项所以加粗和倾斜可以叠加。字号改变用 QSpinBox 的 valueChanged 信号void Note::on_spinChoice_valueChanged(int value) { QTextCursor cursor ui-noteTextEdit-textCursor(); QTextCharFormat format; format.setFontPointSize(value); if (cursor.hasSelection()) { cursor.mergeCharFormat(format); ui-noteTextEdit-setTextCursor(cursor); } else { ui-noteTextEdit-mergeCurrentCharFormat(format); } }颜色选择用 QColorDialog注意要先取当前光标处的颜色作为默认值void Note::on_colorButton_clicked() { QTextCursor cursor ui-noteTextEdit-textCursor(); QColor currentColor ui-noteTextEdit-textColor(); QColor selectedColor QColorDialog::getColor(currentColor, this, 选择颜色); if (selectedColor.isValid()) { QTextCharFormat format; format.setForeground(QBrush(selectedColor)); if (cursor.hasSelection()) { cursor.mergeCharFormat(format); ui-noteTextEdit-setTextCursor(cursor); } else { ui-noteTextEdit-mergeCurrentCharFormat(format); } } }selectionChanged 槽函数负责把选中文本的格式同步回按钮状态。这段逻辑容易写长我拆成独立函数void Note::on_noteTextEdit_selectionChanged() { QTextCursor cursor ui-noteTextEdit-textCursor(); if (!cursor.hasSelection()) { QTextCharFormat format ui-noteTextEdit-currentCharFormat(); ui-boldButton-setChecked(format.fontWeight() QFont::Bold); ui-italicButton-setChecked(format.fontItalic()); ui-underlineButton-setChecked(format.fontUnderline()); return; } bool hasBold false, hasItalic false, hasUnderline false; QTextCursor temp cursor; temp.setPosition(cursor.selectionStart()); while (!temp.atEnd() temp.position() cursor.selectionEnd()) { temp.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, 1); QTextCharFormat f temp.charFormat(); if (f.fontWeight() QFont::Bold) hasBold true; if (f.fontItalic()) hasItalic true; if (f.fontUnderline()) hasUnderline true; if (hasBold hasItalic hasUnderline) break; temp.setPosition(temp.selectionEnd()); } ui-boldButton-setChecked(hasBold); ui-italicButton-setChecked(hasItalic); ui-underlineButton-setChecked(hasUnderline); }注意遍历时用 KeepAnchor 逐字符移动每次读 charFormat。如果选区里只要有一个加粗字符按钮就显示选中这是常见做法。增改功能的数据流用 QSqlTableModel。先建表CREATE TABLE notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, content TEXT, updated_at TEXT );content 字段存 QTextEdit 的 toHtml() 结果。保存时void Note::saveNote(const QString title, const QString html) { QSqlRecord record model-record(); record.setValue(title, title); record.setValue(content, html); record.setValue(updated_at, QDateTime::currentDateTime().toString(Qt::ISODate)); if (currentRow -1) { model-insertRecord(model-rowCount(), record); } else { model-setRecord(currentRow, record); } model-submitAll(); }修改时回填void Note::loadNote(int row) { QModelIndex index model-index(row, 0); QString title model-data(index).toString(); QString html model-data(model-index(row, 2)).toString(); ui-notenext_nameInput-setText(title); ui-noteTextEdit-setHtml(html); }这里有个坑setHtml 会重置撤销栈所以回填后 undo 按钮不可用是正常的。如果你需要保留撤销历史得用 QTextDocument 的 setHtml 后手动清空 undo stack或者干脆接受这个限制。4. 验证请求与成功结果增改闭环的边界用例测试代码写完必须验证。我按“新建→保存→修改→再保存→重启”的顺序走一遍同时覆盖几个边界用例。先验证新建。点击新建按钮stackedWidget 切到第二页标题输入框和 QTextEdit 都为空。输入标题“测试笔记一”在正文里输入“第一行加粗”选中“加粗”两个字点加粗按钮再输入“普通文字”。点保存stackedWidget 切回第一页表格新增一行“测试笔记一”。此时检查数据库sqlite3 ticktask.db SELECT id, title, length(content) FROM notes;应该看到 content 长度大于 0说明 HTML 存进去了。再验证修改。点击表格第一行点修改按钮第二页标题回填“测试笔记一”正文回填带加粗的 HTML。把标题改成“测试笔记一改”在正文末尾追加“新增内容”点保存。表格第一行标题变成“测试笔记一改”数据库里 updated_at 更新。边界用例一空标题。标题输入框留空点保存应该拦截并提示。在 saveNote 里加判断if (title.trimmed().isEmpty()) { QMessageBox::warning(this, 提示, 标题不能为空); return; }边界用例二超长内容。在 QTextEdit 里粘贴 10 万字符点保存。SQLite 的 TEXT 字段理论上限很大但要注意 toHtml() 会膨胀实测 10 万字符纯文本转 HTML 后约 30 万字符保存耗时约 200ms可以接受。如果卡顿考虑分页或延迟保存。边界用例三选区跨格式。输入“加粗普通加粗”全选后看加粗按钮状态。按前面的遍历逻辑只要有一个加粗字符按钮就显示选中。这是预期行为但如果你希望“全加粗才显示选中”把 hasBold 的判断改成“所有字符都加粗”即可。边界用例四撤销重做。输入文字后点撤销内容回退点重做内容恢复。注意 setHtml 回填后撤销栈被清空这是 Qt 的默认行为不是 bug。边界用例五插图后保存。插入一张 PNG保存重启应用图片应该还在。如果图片丢失检查 addResource 时用的 URL 是否和 setHtml 后解析的一致。我踩过的坑是插入时用file:///image_xxx.png但 setHtml 后 Qt 会尝试按相对路径解析导致图片找不到。解决办法是把图片转成 base64 内嵌或者保存时把图片单独存文件并在 HTML 里用绝对路径。验证 AI 辅助通道。把 selectionChanged 的遍历逻辑丢给模型问“这段遍历有没有性能问题”让它审查。如果返回建议比如“可以用 QTextFragment 迭代代替逐字符移动”就采纳优化。这样 AI 不只是生成还参与审查。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth这一节对照真实报错给出排查路径。先列一个对照表报错常见原因排查动作401 UnauthorizedKey 错误或缺失检查 Authorization 头确认 Bearer 后有空格local proxy failed本地网络配置问题检查系统代理设置确认 API 地址可直连reading choices 为空请求体格式错误检查 messages 数组和 model 字段OAuth 相关报错认证方式不匹配确认使用 API Key 而非 OAuth 流程401 是最常见的。如果你用 curl 测试返回 401先确认 Key 有没有复制完整。TaoToken 的 Key 通常以 sk- 开头长度较长容易漏掉尾部字符。另外检查请求头-H Authorization: Bearer sk-你的KeyBearer 和 Key 之间必须有一个空格Key 后面不能有换行。如果你把 Key 写在 JSON 配置文件里注意 JSON 字符串不能有未转义的特殊字符。local proxy failed 通常出现在本地网络环境有代理设置时。排查方法是先确认 API 地址能直连curl -v https://taotoken.net/api/v1/chat/completions如果返回连接超时检查系统代理设置。在 Qt 项目里如果你用 QNetworkAccessManager 发请求它会读取系统代理。可以在代码里显式设置QNetworkProxy proxy; proxy.setType(QNetworkProxy::NoProxy); QNetworkProxy::setApplicationProxy(proxy);reading choices 为空说明请求发出去了但响应体里没有 choices 字段。常见原因是 model 字段写错或者 messages 格式不对。检查请求体{ model: claude-sonnet-4-20250514, messages: [{role: user, content: 测试}] }model 必须是模型对话页里列出的可用模型 ID不能自己编。messages 必须是数组每个元素有 role 和 content。OAuth 报错通常是因为你用了 OAuth 流程而不是 API Key。TaoToken 的 API 通道用 Bearer Token 认证不需要 OAuth。如果你在代码里看到 OAuth 相关逻辑删掉换成 Authorization 头。还有一个 Qt 特有的坑QSqlTableModel 的 submitAll 返回 false 但不报错。排查方法是检查 model-lastError()if (!model-submitAll()) { qDebug() model-lastError().text(); }常见原因是数据库字段类型不匹配比如 content 字段设了 NOT NULL 但传了空字符串。把字段改成允许 NULL或者在代码里保证 content 不为空。如果你用 Claude Code 或类似工具接入注意 Base URL、Key、Model ID 三件套要写全。Base URL 是 https://taotoken.net/api Key 是控制台创建的Model ID 从模型对话页选。三者缺一不可少一个就会报认证或模型不存在。6. 语义一致 CTA把 AI 辅助编码链路固定下来走到这里笔记模块的文本编辑和增改闭环已经跑通了。QTextEdit 的格式操作、QTextCursor 的选区处理、QSqlTableModel 的增改提交、以及边界用例验证都是可复制的模板。剩下的就是把 AI 辅助通道固定成日常习惯遇到 QTextCursor 遍历记不清参数直接调 API 生成片段遇到 submitAll 失败把 lastError 丢给模型让它解释写完一个槽函数让模型审查有没有遗漏 hasSelection 判断。如果你主要做排障和接入建议把 API Keys 页面和接入文档存书签API Keys 在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。遇到 401 或 reading choices 为空先查文档里的错误码对照。如果你需要验证模型生成效果比如让模型写一个 QTextFragment 迭代的优化版本去模型对话页 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 直接试。如果你打算长期做 Qt 开发、经常需要 AI 辅助生成和审查代码Coding Plan 页面 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 有更稳定的调用方案适合把 AI 辅助固定进日常编码流程。最后说一个实用技巧把 selectionChanged 的遍历逻辑抽成独立函数后加粗、倾斜、下划线三个按钮的同步代码可以合并成一个 updateFormatButtons()减少重复。这个重构我建议在功能跑通后再做不要一开始就追求完美。先把闭环跑起来再优化结构这样课设验收和后续拓展都不耽误。