解决Kivy中文乱码问题:从方块乱码到完美显示
💡 阅读须知:我的所有文章免费。若在阅读时遇到VIP限制无法显示,可私信联系我
在使用 Kivy 开发时,代码逻辑完全正确但中文显示为「□□□」方块,是新手最常遇到的“劝退”问题。其根本原因在于Kivy 默认内置的 Roboto 字体仅包含拉丁字符集,当渲染引擎尝试绘制中文字符时,因找不到对应字形而回退为空白占位符。
解决思路非常统一:显式指定一个包含中文字形的字体文件。本文优化了三种主流方案的实现细节,修复了原稿中的路径兼容性与全局配置误区,并补充了跨平台打包的关键注意事项。
⚠️核心避坑前置提醒
- 禁止硬编码 Windows 绝对路径用于移动端:
C:/Windows/Fonts/在安卓/iOS 上不存在,直接打包必崩;- 路径分隔符统一使用
/:Python 字符串中\是转义符,Windows 路径必须写作C:/Windows/Fonts/msyh.ttc或使用原始字符串r'C:\Windows\Fonts\msyh.ttc';- TTC 与 TTF 的区别:
.ttc是字体集合文件(含多个字重),Kivy 支持但部分旧版本可能解析异常,跨平台推荐优先使用.ttf单字体文件;- Config.set 必须在所有 Kivy 模块导入之前执行:否则全局默认字体设置无效。
🔤 方案一:调用系统字体(仅限本地开发调试)
适用场景:Windows/macOS/Linux 本机运行验证,快速测试中文渲染效果。
局限性:不可用于打包分发,目标设备字体路径不一致会导致崩溃。
from kivy.app import App from kivy.uix.label import Label from kivy.core.text import LabelBase import platform # 根据操作系统动态获取系统中文字体路径,避免跨系统报错 system = platform.system() if system == 'Windows': font_path = 'C:/Windows/Fonts/msyh.ttc' elif system == 'Darwin': # macOS font_path = '/System/Library/Fonts/PingFang.ttc' else: # Linux font_path = '/usr/share/fonts/truetype/wqy/wqy-microhei.ttc' LabelBase.register(name='SysChinese', fn_regular=font_path) class MyApp(App): def build(self): return Label( text="本机调试中文显示正常", font_name='SysChinese', font_size=24 ) if __name__ == '__main__': MyApp().run()💡提示
此方案仅作为开发阶段的临时手段,任何需要分发的应用都应使用方案二。
📦 方案二:嵌入项目字体(跨平台打包唯一推荐)
适用场景:EXE、APK、IPA 打包分发,确保所有设备中文显示一致。
核心原则:字体文件随项目一起打包,使用相对路径加载。
1. 项目结构规范
MyKivyApp/ ├── main.py ├── assets/ │ └── SimHei.ttf # 将字体放入项目资源目录 └── buildozer.spec # 安卓打包配置2. 代码实现(使用相对路径)
import os from kivy.app import App from kivy.uix.label import Label from kivy.core.text import LabelBase # 基于当前脚本位置动态拼接字体路径,兼容任意运行环境 BASE_DIR = os.path.dirname(os.path.abspath(__file__)) font_path = os.path.join(BASE_DIR, 'assets', 'SimHei.ttf') LabelBase.register(name='AppChinese', fn_regular=font_path) class MyApp(App): def build(self): return Label( text="跨平台中文显示正常", font_name='AppChinese', font_size=24 ) if __name__ == '__main__': MyApp().run()3. Buildozer 打包关键配置
在buildozer.spec的[app]节点下,必须将字体扩展名加入打包白名单:
# 原始配置通常不包含 ttf/ttc,需手动补充 source.include_exts = py,png,jpg,kv,atlas,ttf,ttc,json # 同时确认 assets 目录被包含 source.include_patterns = assets/*⚠️字体版权警告
微软雅黑(msyh.ttc)、SimHei 等系统字体仅限本机使用,嵌入 APK/IPA 分发存在法律风险。推荐使用开源免费商用字体:思源黑体(Source Han Sans)、文泉驿微米黑、阿里巴巴普惠体。
⚙️ 方案三:全局默认中文字体(复杂项目必备)
适用场景:多页面、多控件项目,避免逐个设置font_name。
关键修正:Config.set()必须在from kivy.app import App之前执行,否则配置不生效。
# ✅ 第一步:在所有 Kivy 模块导入之前设置全局默认字体 from kivy.config import Config Config.set('kivy', 'default_font', 'AppChinese') Config.write() # ✅ 第二步:再导入其他 Kivy 模块并注册字体 import os from kivy.app import App from kivy.uix.label import Label from kivy.uix.button import Button from kivy.core.text import LabelBase BASE_DIR = os.path.dirname(os.path.abspath(__file__)) font_path = os.path.join(BASE_DIR, 'assets', 'SimHei.ttf') LabelBase.register(name='AppChinese', fn_regular=font_path) class MyApp(App): def build(self): # 无需指定 font_name,自动继承全局默认中文字体 return Label(text="全局默认中文,无需逐个设置", font_size=24) if __name__ == '__main__': MyApp().run()💡注意
Config.write()会将配置写入用户目录下的~/.kivy/config.ini,属于持久化设置。若仅需运行时生效,可省略Config.write(),避免污染开发环境全局配置。
🎮 实战:修复后的猜数字小游戏(跨平台安全版)
以下代码已移除硬编码路径,采用方案二+方案三组合,可直接打包为 APK/EXE:
# === 全局配置必须在最顶部 === from kivy.config import Config Config.set('kivy', 'default_font', 'GameFont') import os, random from kivy.app import App from kivy.uix.boxlayout import BoxLayout from kivy.uix.label import Label from kivy.uix.textinput import TextInput from kivy.uix.button import Button from kivy.core.text import LabelBase # 动态路径,兼容 PC 与手机 BASE_DIR = os.path.dirname(os.path.abspath(__file__)) font_path = os.path.join(BASE_DIR, 'assets', 'SourceHanSansCN-Regular.ttf') LabelBase.register(name='GameFont', fn_regular=font_path) class GuessNumberGame(App): def build(self): self.target = random.randint(1, 100) self.count = 0 layout = BoxLayout(orientation='vertical', padding=30, spacing=20) self.title_lbl = Label(text="🎯 猜数字小游戏", font_size=26, bold=True) self.hint_lbl = Label(text="请输入 1-100 的数字开始猜测", font_size=18, color=(0.2, 0.6, 0.8, 1)) self.input = TextInput(hint_text="输入数字", font_size=18, input_type='number', multiline=False) guess_btn = Button(text="提交猜测", font_size=18, background_color=(0.3, 0.7, 0.3, 1)) guess_btn.bind(on_press=self.check_guess) reset_btn = Button(text="重新开始", font_size=18, background_color=(0.8, 0.4, 0.4, 1)) reset_btn.bind(on_press=self.reset_game) for w in [self.title_lbl, self.hint_lbl, self.input, guess_btn, reset_btn]: layout.add_widget(w) return layout def check_guess(self, _): try: val = int(self.input.text.strip()) self.count += 1 if not (1 <= val <= 100): self.hint_lbl.text = f"⚠️ 请输入 1-100!已猜 {self.count} 次" elif val < self.target: self.hint_lbl.text = f"📉 太小了!再大一点~ 已猜 {self.count} 次" elif val > self.target: self.hint_lbl.text = f"📈 太大了!再小一点~ 已猜 {self.count} 次" else: self.hint_lbl.text = f"🎉 恭喜猜中!共 {self.count} 次,点重新开始继续" except ValueError: self.hint_lbl.text = "❌ 请输入有效数字" finally: self.input.text = "" def reset_game(self, _): self.target = random.randint(1, 100) self.count = 0 self.hint_lbl.text = "🔄 游戏已重置,请输入 1-100 开始猜测" self.input.text = "" if __name__ == '__main__': GuessNumberGame().run()