
1. 为什么 setFontFamily 在 QTextEdit 里会“失灵”如果你正在用 PyQt5 写一个自定义富文本编辑器大概率会遇到这个场景工具栏上放了一个字体下拉框用户选中“宋体”或者“微软雅黑”你调用setFontFamily之后光标处新输入的文字字体确实变了但一旦文档是通过setHtml加载进来的或者光标停在已有文字中间字体设置就像石沉大海界面毫无反应。更诡异的是打开保存后的 HTML 一看font-family里居然塞了两个字体名浏览器渲染时只认第一个第二个形同虚设。这个问题的核心其实不是setFontFamily这个 API 本身有 bug而是 QTextEdit 的富文本模型里存在三层格式作用域光标当前字符格式、选区字符格式、文档默认格式。你调用currentCharFormat()拿到的是光标位置的“当前格式快照”mergeCharFormat只把这个快照合并到光标选区或后续输入。如果文档是通过setHtml加载的HTML 里的style属性会生成一套独立的字符格式和你后来设置的格式发生叠加而不是替换。叠加的结果就是font-family出现多个值Qt 内部按优先级取第一个导致你新设的字体被“压”在后面看起来就是不生效。我试过在一个加载了默认 HTML 模板的编辑器里直接调setFontFamily(宋体)结果光标处输入的新字还是默认字体。后来把charFormat打印出来才发现fontFamily()返回的是空字符串而fontFamilies()返回的是一个包含旧字体的列表。也就是说Qt5 后期版本里setFontFamily和setFontFamilies操作的是两个不同的内部字段只调其中一个另一个字段保留旧值合并时就会产生冲突。所以排查方向很明确先确认你操作的是光标格式还是文档默认格式再确认fontFamily和fontFamilies是否同步设置最后检查setHtml加载的内容是否自带style覆盖。下面我会从环境准备开始一步步给出可复制的配置和验证代码帮你把字体设置彻底跑通。2. TaoToken 前置准备模型对话与 API Key 获取在动手改代码之前如果你打算在编辑器里接入 AI 辅助润色、自动排版或者字体推荐功能可以先把 TaoToken 的调用环境准备好。TaoToken 是一个面向开发者的模型调用平台支持对话、代码生成和 Agent 工作流适合在 PyQt5 桌面应用里做后端能力补充。你不需要把它想得太复杂就当成一个可以发 HTTP 请求的模型接口就行。第一步打开模型对话页面确认你要用的模型 ID。地址是https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmodel_chat在这个页面里你可以直接和模型对话测试它能不能理解“把这段文字改成宋体”或者“帮我生成一段富文本 HTML 模板”。测试通过后再去控制台创建 API Keyhttps://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentapi_keys创建 Key 的时候建议单独建一个项目名字就叫pyqt-editor方便后面排查调用量。Key 只显示一次复制后先存到环境变量里不要硬编码进 PyQt5 的源码。你可以这样设置export TAOTOKEN_API_KEY你的Key如果你用的是 Windows PowerShell$env:TAOTOKEN_API_KEY你的Key接下来是 Base URL。TaoToken 的 API 入口是https://taotoken.net/api注意这个地址后面不要加 UTM 参数直接作为base_url使用。模型 ID 根据你在模型对话页面选的那个填比如gpt-4o或者claude-3-5-sonnet之类的字符串。如果你后面要接 Claude Code 或者做长期编码 Agent可以看一下 Coding Plan 页面https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcoding_plan接入文档在https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdoc把这三样东西准备好Base URL、API Key、Model ID。后面在 PyQt5 里发请求时就是标准的 OpenAI 兼容格式用requests或者httpx都能调。这一步不涉及任何网络代理工具就是普通的 HTTPS 请求你在公司内网或者家庭宽带下都能直接访问。3. 可复制配置QTextEdit 字体设置的正确写法现在进入正题。假设你已经有一个 PyQt5 的富文本编辑器窗口里面有一个QTextEdit叫self.text_edit工具栏上有一个字体下拉框QFontComboBox信号连接到set_font_family方法。下面这份代码可以直接复制到你的项目里路径和变量名按你自己的改。先看核心的字体设置方法。关键点是同时调用setFontFamily和setFontFamilies并且要区分“光标处设置”和“选区设置”。from PyQt5.QtWidgets import QApplication, QMainWindow, QTextEdit, QFontComboBox, QToolBar from PyQt5.QtGui import QTextCharFormat, QFont from PyQt5.QtCore import Qt class RichEditor(QMainWindow): def __init__(self): super().__init__() self.text_edit QTextEdit() self.setCentralWidget(self.text_edit) toolbar QToolBar() self.addToolBar(toolbar) self.font_box QFontComboBox() self.font_box.currentFontChanged.connect(self.set_font_family) toolbar.addWidget(self.font_box) # 模拟通过 setHtml 加载默认内容 self.text_edit.setHtml( p stylefont-family:微软雅黑;这是默认内容字体是微软雅黑。/p p这是第二段没有内联字体样式。/p ) def set_font_family(self, font: QFont): family font.family() cursor self.text_edit.textCursor() # 关键同时设置 fontFamily 和 fontFamilies char_format QTextCharFormat() char_format.setFontFamily(family) char_format.setFontFamilies([family]) if cursor.hasSelection(): # 有选区合并到选区 cursor.mergeCharFormat(char_format) else: # 无选区合并到光标当前格式影响后续输入 self.text_edit.mergeCurrentCharFormat(char_format) # 强制刷新避免界面延迟 self.text_edit.setCurrentCharFormat(char_format)这段代码里setFontFamily和setFontFamilies同时调用是解决“两个字体”问题的核心。setFontFamily设置的是单个字体名setFontFamilies设置的是字体族列表。Qt 在生成 HTML 时如果两个字段不一致就会把两个都写进font-family导致浏览器取第一个。两个都设成同一个值生成的 HTML 里就只有一个字体名了。另外注意mergeCurrentCharFormat和cursor.mergeCharFormat的区别。前者作用于编辑器当前光标格式后者作用于光标选区。如果你在无选区的情况下只调cursor.mergeCharFormat新输入的文字可能不会应用新格式因为光标格式没有被更新。所以无选区时用mergeCurrentCharFormat更稳妥。如果你还想设置文档默认字体比如让整个编辑器的初始字体就是宋体可以在初始化时加default_format QTextCharFormat() default_format.setFontFamily(宋体) default_format.setFontFamilies([宋体]) self.text_edit.document().setDefaultFont(QFont(宋体)) self.text_edit.setCurrentCharFormat(default_format)document().setDefaultFont影响的是没有显式格式的文本而setCurrentCharFormat影响的是光标后续输入。两者配合才能覆盖setHtml加载后留下的格式残留。如果你在编辑器里集成了 TaoToken 做 AI 润色可以在发送请求前把当前 HTML 取出来让模型返回修改后的 HTML再用setHtml重新加载。但重新加载后记得再调一次上面的默认格式设置否则字体又会回到 HTML 内联样式。请求示例import os import requests def polish_with_taotoken(html_content: str) - str: api_key os.environ.get(TAOTOKEN_API_KEY) url https://taotoken.net/api/chat/completions headers { Authorization: fBearer {api_key}, Content-Type: application/json } payload { model: gpt-4o, messages: [ {role: system, content: 你是一个富文本排版助手只返回修改后的HTML不要解释。}, {role: user, content: html_content} ] } resp requests.post(url, jsonpayload, headersheaders, timeout30) resp.raise_for_status() return resp.json()[choices][0][message][content]这段代码里的base_url就是https://taotoken.net/api模型 ID 按你实际选的填。注意不要在代码里写死 Key用环境变量读取。4. 验证请求与成功结果从光标到 HTML 的完整检查配置写完之后怎么确认字体真的生效了不能只看界面因为界面可能因为光标位置不同而显示不一致。你需要从三个层面验证光标当前格式、选区格式、生成的 HTML。先写一个调试方法把当前光标格式打印出来def debug_current_format(self): cursor self.text_edit.textCursor() char_format cursor.charFormat() print(fontFamily:, char_format.fontFamily()) print(fontFamilies:, char_format.fontFamilies()) print(hasSelection:, cursor.hasSelection()) print(selectedText:, cursor.selectedText()[:50])把光标放到setHtml加载的第一段文字中间调用debug_current_format。如果fontFamily返回空字符串而fontFamilies返回[微软雅黑]说明 HTML 内联样式只写进了fontFamilies字段fontFamily是空的。这时候你只调setFontFamily(宋体)合并后fontFamily变成宋体但fontFamilies还是微软雅黑生成的 HTML 就会出现两个字体。再调一次set_font_family把字体设成宋体然后再次debug_current_format。正确的输出应该是fontFamily: 宋体 fontFamilies: [宋体] hasSelection: False如果fontFamilies里还有旧字体说明你的char_format是从currentCharFormat()拿的而不是新建的QTextCharFormat()。用currentCharFormat()会继承旧格式必须显式覆盖两个字段。我建议直接新建QTextCharFormat只设你需要的属性避免旧值干扰。接下来验证 HTML 输出。在设置字体后调用html self.text_edit.toHtml() print(html)在输出的 HTML 里搜索font-family。正确的片段应该类似span stylefont-family:宋体;这是默认内容/span如果看到font-family:宋体,微软雅黑;或者font-family:微软雅黑,宋体;说明两个字段没有同步需要回到上一节检查setFontFamilies是否被调用。还有一个容易忽略的点setHtml加载的内容如果带有style块里面的 CSS 规则会覆盖内联格式。你可以用self.text_edit.document().setDefaultStyleSheet()清空默认样式表再重新设置字体。或者在setHtml之后遍历文档块逐个清除格式doc self.text_edit.document() block doc.begin() while block.isValid(): for fragment in block: pass block block.next()更简单的做法是在setHtml之后调用一次全选然后setCurrentCharFormat统一格式cursor self.text_edit.textCursor() cursor.select(cursor.Document) fmt QTextCharFormat() fmt.setFontFamily(宋体) fmt.setFontFamilies([宋体]) cursor.mergeCharFormat(fmt) self.text_edit.setTextCursor(cursor)这样整个文档的字体就被统一替换了不会残留旧字体。验证成功后你可以把这段逻辑封装成apply_default_font方法在每次setHtml后调用。如果你用 TaoToken 做 AI 润色润色返回的 HTML 可能自带字体样式。你可以在请求的 system prompt 里明确要求“不要添加 font-family 样式”或者在收到结果后先清空格式再应用默认字体。这样能保证编辑器里的字体始终受你的工具栏控制而不是被模型返回的 HTML 带偏。5. 本篇常见错排查401、local proxy failed、reading choices 与 OAuth即使字体设置代码写对了实际跑起来还是可能遇到各种报错。下面这几个是我在 PyQt5 编辑器接入模型接口时踩过的坑对照着看能省不少时间。401 Unauthorized这个最常见一般是 API Key 没读到或者写错了。先检查环境变量import os print(os.environ.get(TAOTOKEN_API_KEY))如果输出None说明环境变量没生效。PyCharm 里要在 Run Configuration 的 Environment variables 里加VS Code 里要在.env文件或者 launch.json 里配。另外注意 Key 前后不要有空格复制的时候容易带上换行符。用strip()处理一下api_key os.environ.get(TAOTOKEN_API_KEY, ).strip()local proxy failed这个报错通常出现在你本地开了某些网络工具导致requests走了本地代理端口但代理没启动或者端口不对。PyQt5 应用里如果用了requests它会自动读取HTTP_PROXY和HTTPS_PROXY环境变量。你可以在代码里显式禁用代理session requests.Session() session.trust_env False resp session.post(url, jsonpayload, headersheaders, timeout30)或者在请求里加proxies{http: None, https: None}。这样就不会走本地代理直接连 TaoToken 的 API 地址。reading choices 报错这个一般是你解析响应时resp.json()里没有choices字段。先打印完整响应print(resp.status_code) print(resp.text)如果返回的是{error: {message: ...}}说明请求参数有问题比如模型 ID 写错了或者messages格式不对。TaoToken 的接口是 OpenAI 兼容格式messages必须是列表每个元素有role和content。如果你把content写成了列表或者 None就会报错。另外注意model字段要和你在模型对话页面选的一致不要自己编。OAuth 相关报错如果你在 PyQt5 里集成了 Claude Code 或者 Codex 的 OAuth 登录流程可能会遇到OAuth token expired或者invalid_grant。这类问题一般是因为本地缓存的 token 过期了需要重新走授权。如果你只是用 API Key 调用不涉及 OAuth可以忽略。但如果你确实在用 Claude Code 的 OAuth检查一下~/.claude/settings.json或者auth.json里的配置。Base URL 要指向https://taotoken.net/apiKey 用你创建的 API KeyModel ID 填你选的模型。三件套缺一不可{ base_url: https://taotoken.net/api, api_key: 你的Key, model: claude-3-5-sonnet }如果你用的是 Cline 或者 MCP 类的工具配置里同样要写全 Base URL、Key、Model ID。Cline 的 MCP 配置一般在cline_mcp_settings.json里Codex 的auth.json里也要对应填。CC Switch 这类切换工具检查它有没有把 Base URL 覆盖成默认值。任何一项缺失或者写错都会导致请求失败而不是字体问题。最后提醒一点字体不生效和接口报错是两个独立的问题。先把字体设置跑通确认toHtml()输出正确再去接模型接口。不要一边调字体一边调 API否则报错来源分不清。6. 语义一致 CTA继续深入模型对话与接入文档字体设置跑通之后你的 PyQt5 富文本编辑器已经能正确响应工具栏的字体切换了。接下来如果想让编辑器更智能比如根据选中文字自动推荐字体、自动生成排版 HTML或者接入 AI 做内容润色可以继续用 TaoToken 的模型对话能力做测试。模型对话页面可以直接验证模型能不能理解你的排版需求https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmodel_chat接入文档里有完整的请求示例和参数说明包括流式输出、多轮对话和错误码解释https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdoc如果你打算把 AI 润色做成编辑器的长期功能或者用 Agent 自动处理文档格式可以看一下 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcoding_planAPI Key 的管理在控制台https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentapi_keys回到字体问题本身记住那个核心结论setFontFamily和setFontFamilies要同时调用并且优先新建QTextCharFormat而不是从currentCharFormat()继承。每次setHtml之后用全选加mergeCharFormat统一格式避免内联样式残留。这样你的编辑器不管加载什么 HTML字体都能被工具栏正确控制。