Kivy 是什么,怎么装
Kivy 是跨平台的开源 GUI 框架 ,和 PySide6/PyQt6 最大的区别是:它不用操作系统的原生控件,而是自己用 OpenGL 把每个控件画出来 。好处是 Windows、Linux、macOS、Android、iOS 上长得一模一样、支持多点触控;代价是界面不像原生程序,而且中文字体、无障碍、系统主题都得自己管。
选型上的三条硬判断(本篇都实测过):
只做桌面、要原生观感和成熟控件表(表格、树、菜单、打印)→ 选 Qt(见《PySide6与PyQt6入门》)。
要一份代码同时上桌面和安卓/iOS,或者做触屏、手势、Kiosk 类应用 → 选 Kivy。
只想在标准库里挑一个 → 那还是 Tkinter(见《Tkinter》)。
体积对比实测:Kivy 全家桶约 38 MB (kivy 包 14.8 MB + 依赖 DLL 22.8 MB),而 PySide6 是 650 MB、PyQt6 是 214 MB。
安装:Python 3.14 装不上,请用 3.12/3.13
1 2 3 4 5 6 7 8 9 10 11 在 Python 3.14.10 上(实测): ERROR: Could not find a version that satisfies the requirement kivy_deps.sdl2_dev~=0.7.0 (from versions: 0.8.1) ERROR: No matching distribution found for kivy_deps.sdl2_dev~=0.7.0 note: This error originates from a subprocess, and is likely not a problem with pip. ERROR: Failed to build 'Kivy' when installing build dependencies for kivy 强制只用 wheel 时更直白: pip install --only-binary :all: Kivy ERROR: Could not find a version that satisfies the requirement Kivy (from versions: none) ERROR: No matching distribution found for Kivy
装完第一次 import,会看到一堆日志
1 2 3 import kivyfrom kivy.app import Appfrom kivy.uix.label import Label
1 2 3 4 5 6 7 8 9 10 11 [INFO ] [Kivy ] v2.3.1 [INFO ] [Kivy ] Installed at "...\site-packages\kivy\__init__.py" [INFO ] [Python ] v3.12.16 | packaged by Anaconda, Inc. [INFO ] [Factory ] 195 symbols loaded [INFO ] [Image ] Providers: img_tex, img_dds, img_sdl2 (img_pil, img_ffpyplayer ignored) [INFO ] [Window ] Provider: sdl2 [INFO ] [GL ] Using the "OpenGL" graphics system [INFO ] [GL ] GLEW initialization succeeded [INFO ] [GL ] OpenGL version <b'4.6.0 NVIDIA 621.01'> [INFO ] [GL ] OpenGL renderer <b'NVIDIA GeForce RTX 3070 Ti Laptop GPU/PCIe/SSE2'> [INFO ] [Window ] virtual keyboard not allowed, single mode, not docked
这堆 [INFO] 是 Kivy 的日志系统打的,默认级别就是 info ,正式程序里建议关掉:
1 2 import osos.environ["KIVY_NO_CONSOLELOG" ] = "1"
Factory ] 195 symbols loaded 说明控件是靠「工厂注册」动态创建的,KV 语言里写的 Label:、Button: 就是这些符号名。
Providers: img_tex, img_dds, img_sdl2 (img_pil, img_ffpyplayer ignored) 表示没装 Pillow 时图片走 SDL2;装了 Pillow 会多出 img_pil。
依赖 DLL 不在 site-packages 里,实测在环境根目录:
1 2 3 kivy-env\share\angle\bin\libEGL.dll kivy-env\share\glew\bin\glew32.dll kivy-env\share\sdl2\bin\SDL2.dll、SDL2_image.dll、SDL2_mixer.dll、SDL2_ttf.dll
记这一点是为了打包:PyInstaller 不会自动把 share 下的 DLL 收进去,见「打包」一节。
一个必须知道的脾气:Kivy 会解析 sys.argv
1 2 python gui_note_kivy.py --selftest -> Kivy 打印自己的 usage(-h/--help/-d/--debug/--size=640x480/--dpi=96...)然后退出,程序根本没跑
第一个 Kivy 应用
Python 写法:App + build()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 from kivy.app import Appfrom kivy.uix.label import Labelfrom kivy.uix.boxlayout import BoxLayoutfrom kivy.uix.button import Buttonclass MyApp (App ): title = "Hello Kivy" def build (self ): lay = BoxLayout(orientation="vertical" ) lay.add_widget(Label(text="天下无双" , font_size=32 )) lay.add_widget(Button(text="点我" , size_hint_y=None , height=48 )) return lay MyApp().run()
KV 写法:把界面从代码里拆出去
KV 是 Kivy 独有的声明式语言,一个类名一块规则,缩进表示父子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 from kivy.lang import BuilderBuilder.load_string(""" <MyRoot>: orientation: 'vertical' editor: ed Label: text: '天下无双' font_size: 32 size_hint_y: None height: 48 TextInput: id: ed multiline: True Button: text: '保存' size_hint_y: None height: 48 on_press: root.do_save() """ )class MyRoot (BoxLayout ): def do_save (self ): print (self .editor.text)
规则名(<MyRoot>)要和 Python 类同名,Kivy 会自动把类注册进 Factory。
id: ed 是局部名字 ,只能在这个 KV 文件里用;要让 Python 拿到,得在根上显式暴露:editor: ed。
实测暴露出来的对象类型:
1 2 3 4 5 通过 KV 里 `editor: la` 拿到的对象类型 = WeakProxy 是弱代理吗 = True isinstance(p.label_a, Label) = True 和 children[0] 是同一个对象吗 = False 直接 ids 访问 = WeakProxy ids 里有哪些 = ['la']
WeakProxy 是 Kivy 最容易踩的坑之一 :实测 p.label_a is p.children[0] 为 False(代理不是原对象),但 isinstance(p.label_a, Label) 为 True。所以别用 is 比较控件、也别把代理对象当字典键长期持有;需要稳定引用时从 self.ids.xxx 或 children 现取现用。
KV 里能直接写表达式和事件回调(on_press: root.do_save()),改界面不用动 Python,这是 Kivy 开发效率的主要来源。
事件循环与 Clock
Kivy 只有一个主线程事件循环,所有定时、动画、跨线程回调都走 Clock:
1 2 3 4 5 from kivy.clock import ClockClock.schedule_once(lambda dt: print ("0.5 秒后" ), 0.5 ) ev = Clock.schedule_interval(lambda dt: print (dt), 1.0 ) ev.cancel()
1 回调线程号与主线程一致 = True # 在子线程里 schedule_once,回调仍在主线程
dt 是「距离上次触发的时间差」,第一次可能不是精确的 1.0,实测就是这样设计的(用于做帧无关的动画)。
Clock 是从其它线程安全碰 UI 的唯一入口 ,实测对比:
1 2 子线程里 Clock.schedule_once -> 回调线程号与主线程一致 = True 子线程里直接 add_widget -> TypeError: Cannot create graphics instruction outside the main Kivy thread
Kivy 是硬拦(抛异常),Qt 是不拦(可能随机崩)。前者更安全。
不用 run() 也能手动推帧,本篇的自检就是这么跑的:
1 2 3 for _ in range (30 ): Clock.tick() time.sleep(0.005 )
属性系统(KProperty):改了会自动通知
1 2 3 4 5 6 7 8 9 10 11 12 13 from kivy.properties import NumericProperty, StringProperty, ObjectProperty, ListPropertyclass Demo (BoxLayout ): n = NumericProperty(0 ) name = StringProperty("" ) obj = ObjectProperty(None ) items = ListProperty([]) d = Demo() d.bind(n=lambda ins, v: print ("n 变成" , v)) d.n = 7
1 2 KProperty 绑定回调 = [7] 类型 = int 给 NumericProperty 赋字符串 -> ValueError: could not convert string to float: 'abc'
这是 Kivy 相对 Qt 最独特的设计:属性自带变更通知 ,不需要像 Qt 那样为每个值写一个 signal。
它会做类型转换和校验,实测 NumericProperty 收字符串直接 ValueError;ListProperty/DictProperty 改动元素时必须整体赋值 才会触发回调(原地 append 不发事件,这是常见 bug 源)。
动画
1 2 3 4 from kivy.animation import Animationanim = Animation(x=500 , duration=0.2 , t="linear" ) + Animation(x=0 , duration=0.2 ) anim.start(button)
1 动画 x 采样前 5 = [0, 30.6, 62.2, 93.3, 124.7] 峰值 = 500.0 结束 = 0.0
两个 Animation 用 + 串成顺序播放,用 ,(元组)可以并行;t= 是缓动曲线名(linear、in_quad、out_back…)。
采样值是按帧推进的,所以数值不会正好落在 33.3 这种整数上,测试时别写死断言。
完整小工具:记事本
需求和《PySide6与PyQt6入门》里那份完全一样,方便横向比较:新建 / 打开 / 保存 / 另存为、查找替换、撤销、状态栏字数、脏标记、关闭自动存盘 。
Kivy 这边有两个「Qt 免费送、这里要自己写」的地方:TextInput 没有撤销栈 ,也没有菜单/工具栏/状态栏 这套主窗口结构。所以下面的代码里,工具栏是一排 Button,状态栏是一个 BoxLayout 里的两个 Label,撤销是自己维护的 history 列表。
KV:界面结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 <NoteRoot>: orientation: 'vertical' editor: editor status: status count: count BoxLayout: size_hint_y: None height: 44 Button: text: '新建' on_press: root.new_file() Button: text: '打开' on_press: root.open_file() Button: text: '保存' on_press: root.save_file() Button: text: '查找替换' on_press: root.open_find() Button: text: '撤销' on_press: root.undo() TextInput: id: editor multiline: True size_hint_y: None height: 320 font_size: 16 BoxLayout: size_hint_y: None height: 32 Label: id: status text: '就绪' halign: 'left' size_hint_x: 3 text_size: self.size Label: id: count text: '0 字' halign: 'right' size_hint_x: 1 text_size: self.size
每个 Button 的 on_press 直接写 root.xxx(),root 就是这个类本身,不需要像 Qt 那样先建 QAction 再 connect。
Label 想改对齐必须同时给 text_size: self.size,否则 halign 无效——这是 Kivy 文档里写得很靠后、但几乎人人踩的一条。
Python:逻辑与属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 from kivy.app import Appfrom kivy.properties import BooleanProperty, ListProperty, StringPropertyfrom kivy.uix.boxlayout import BoxLayoutBuilder.load_string(KV) class NoteRoot (BoxLayout ): dirty = BooleanProperty(False ) path = StringProperty("" ) history = ListProperty([]) def on_text_changed (self, *args ): self .count.text = "%d 字" % len (self .editor.text) self .dirty = True def new_file (self ): self .history.append(self .editor.text) self .editor.text = "" self .path = "" self .status.text = "已新建" def open_file (self, fname=None ): fname = fname or os.path.join(WORK, "kivy_note.txt" ) if not os.path.exists(fname): self .status.text = "文件不存在" return False self .history.append(self .editor.text) with open (fname, encoding="utf-8" ) as f: self .editor.text = f.read() self .path = fname self .dirty = False self .status.text = "已打开 " + os.path.basename(fname) return True def save_file (self, fname=None ): fname = fname or (self .path or os.path.join(WORK, "kivy_note.txt" )) self .history.append(self .editor.text) with open (fname, "w" , encoding="utf-8" ) as f: f.write(self .editor.text) self .path = fname self .dirty = False self .status.text = "已保存 " + os.path.basename(fname) return fname def undo (self ): if not self .history: self .status.text = "没有可撤销的记录" return False self .editor.text = self .history.pop() self .status.text = "已撤销" return True
三个类属性 dirty/path/history 是 KProperty ,改值会自动通知绑定者(KV 里也能直接绑),所以不需要信号。
editor 这个属性是 KV 里 editor: editor 暴露出来的 WeakProxy,self.editor.text = ... 就是给真控件赋值。
撤销的实现只有 4 行:把旧文本压进 history,undo() 弹回来。代价是它只记文本快照 ,不记光标位置,也没有 Qt 那种「输入过程中自动合并」的能力——这也是为什么正式项目里宁可用 Qt 的 QTextDocument。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 def open_find (self ): content = BoxLayout(orientation="vertical" , spacing=8 , padding=12 ) find = TextInput(hint_text="查找" , size_hint_y=None , height=40 ) repl = TextInput(hint_text="替换" , size_hint_y=None , height=40 ) row = BoxLayout(size_hint_y=None , height=44 , spacing=8 ) btn_do = Button(text="全部替换" ) btn_close = Button(text="关闭" ) row.add_widget(btn_do) row.add_widget(btn_close) for wdg in (find, repl, row): content.add_widget(wdg) popup = Popup(title="查找替换" , content=content, size_hint=(0.6 , 0.5 )) btn_close.bind(on_press=popup.dismiss) def do_replace (*_ ): text = self .editor.text n = text.count(find.text) self .editor.text = text.replace(find.text, repl.text) self .status.text = "替换 %d 处" % n popup.dismiss() return n btn_do.bind(on_press=do_replace) popup.open () return popup, do_replace, find, repl
Kivy 的按钮事件用 bind(on_press=...) 挂,等价于 Qt 的 clicked.connect(...);回调第一个参数固定是触发它的控件 ,所以签名要写 def f(instance) 或 *_。
Popup 不是模态阻塞的:popup.open() 立刻返回,程序继续跑。这点和 Qt 的 QMessageBox.information()(实测阻塞 0.32 秒直到被关闭)行为完全不同,写自动化测试时反而更方便。
App 子类与关闭自动存盘
1 2 3 4 5 6 7 8 9 10 class NoteApp (App ): title = "Kivy 记事本" def build (self ): self .root = NoteRoot() return self .root def on_stop (self ): if self .root.dirty: self .root.save_file(os.path.join(WORK, "kivy_autosave.txt" ))
1 2 可覆写的钩子 = ['build', 'on_start', 'on_pause', 'on_resume', 'on_stop'] 未 run 时 App.get_running_app() = <__main__.Tmp object at 0x...>
on_pause / on_resume 是给移动端用的(切后台/回前台),桌面端基本只用到 on_start 和 on_stop。
App.get_running_app() 在没 run() 之前也能拿到「最后创建的那个 App 实例」,实测如此——所以别指望它能判断「是否已经在跑」。
自检跑完的总账
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Kivy 版本 = 2.3.1 import Kivy 及全部子模块耗时 = 0.836 秒 === Kivy 记事本自检 === build() 之后 Window 尺寸 = (1000, 750) 旋转 = 0 顶层控件 = NoteRoot 子控件数 = 3 编辑器类 = WeakProxy multiline = True font_size = 16 输入后字数标签 = 23 字 dirty = True 保存 = kivy_note.txt 磁盘行数 = 4 dirty = False 改文本后历史长度 = 1 撤销返回 = True 撤销后 = '床前明月光' 替换处数 = 2 状态栏 = 替换 2 处 替换后正文 = '床前太阳光\n疑是地上霜\n举头望太阳\n低头思故乡' 重新打开 = True 首行 = '床前明月光' autosave 存在 = True 内容 = '关闭前又改了' 从 import 到自检结束总耗时 = 0.89 秒
和 Qt 那份对比:功能逐项一致,代码量 200 行左右(Qt 230 行),但启动成本差一个数量级 ——import kivy 就要 0.836 秒,而 Qt 的 QApplication 建好只用 0.076 秒。
子控件数 = 3 是 children 的顺序(Kivy 里 children 是反向 添加顺序,最后 add 的在 index 0),做布局调试时别按直觉数。
中文、字体与常用控件
默认字体不认中文,必须自己注册
这是 Kivy 在中文环境下第一大坑,我把它渲染出来量了:
1 2 3 4 5 6 7 8 from kivy.core.text import Label as CoreLabel, LabelBaseLabelBase.register(name="msyh" , fn_regular=r"C:\Windows\Fonts\msyh.ttc" ) for extra in ({}, {"font_name" : "msyh" }): c = CoreLabel(text="床前明月光中文测试" , font_size=32 , **extra) c.refresh() c.texture.save("out.png" )
1 2 默认 Roboto -> 纹理 126 x 38,不透明像素 1584 # 9 个汉字只占 126px,每字 14px 微软雅黑 -> 纹理 288 x 43,不透明像素 3761 # 每字正好 32px(全角)
把两张纹理贴到深色背景上看,默认字体那 9 个字全部是「□ 里带叉」的 notdef 方块 ,也就是根本没显示中文;注册雅黑之后才是正常汉字。
所以规则是:只要界面里有中文,就先 LabelBase.register(...) 再设 font_name ,或者干脆全局默认:
1 2 3 4 from kivy.core.text import LabelBaseLabelBase.register(name="默认中文" , fn_regular=r"C:\Windows\Fonts\msyh.ttc" , fn_bold=r"C:\Windows\Fonts\msyhbd.ttc" )
1 2 3 # kivy_config 里也可以设,但代码里注册更可靠 [Kivy] label_default_fontname = msyh
一个看图时的注意点:Texture.save() 出来的 PNG 是上下颠倒 的(GL 纹理原点在左下),肉眼检查字形时先翻转,或者像我一样直接比宽高与像素数 这种硬指标。
其它字体相关实测
1 2 3 CoreLabel(font_size=32) 英文 "ABCDEFGHIJKL" -> 纹理 228 x 38 微软雅黑 英文同样内容 -> 纹理 230 x 43 Window.dpi = 120.0 # 系统 125% 缩放
控件清单(实测 Factory 里到底有没有)
1 2 3 4 5 6 7 8 Factory 注册的类数量 = 210(启动日志里的 195 是初始数量,用到更多会补) Button True Label True TextInput True BoxLayout True GridLayout True FloatLayout True ScrollView True Popup True TabbedPanel True Spinner True Slider True ProgressBar True CheckBox True Accordion True FileChooserListView True ScreenManager True Image True Camera True Video True RstDocument True TreeView True StencilView True AnchorLayout True StackLayout True RadioButton False ListAdapter False
没有 RadioButton :Kivy 用 kivy.uix.behaviors.CompoundSelectionBehavior 或 RadioGroup mixin 给一组 ToggleButton 做互斥,实测 CompoundSelectionBehavior 确实在 behaviors 里:
1 2 3 behaviors = ['ButtonBehavior', 'CodeNavigationBehavior', 'CompoundSelectionBehavior', 'CoverBehavior', 'DragBehavior', 'EmacsBehavior', 'FocusBehavior', 'ToggleButtonBehavior', 'TouchRippleBehavior', 'TouchRippleButtonBehavior', ...]
对照 Qt:QCheckBox/QRadioButton/QComboBox/QSpinBox/QDateTimeEdit/QMenuBar/QStatusBar/QTableView 这些 Kivy 都没有对应物 ,要么用 Spinner(下拉)+ TextInput 拼,要么走第三方 KivyMD。
需要更「现代」的观感时社区一般上 KivyMD (Material Design 组件集),本篇没装没测,不展开。
文件选择:没有原生对话框
1 2 3 4 from kivy.uix.filechooser import FileChooserListViewfc = FileChooserListView(path=r"E:\Desktop\Test\gui-probe" ) print (fc.files[:3 ], len (fc.files))
1 2 3 4 path = E:\Desktop\Test\gui-probe files 数量 = 11 前 3 个 = ['E:\\Desktop\\Test', 'E:\\...\\autosave.txt', 'E:\\...\\kivy_autosave.txt'] filters = [](可以设,例如 ['*.txt']) selection 初值 = []
FileChooserListView 是画在窗口里的一个控件 ,不是操作系统弹窗,所以它长得不像系统对话框,也没有「另存为」的保存能力——要保存得自己拼一个 Popup + TextInput 输入文件名。
目录和文件混在 files 里返回,判断类型要自己 os.path.isdir。
事件用 fc.bind(on_selection=lambda inst, value: ...) 挂(KProperty 事件),别用 hasattr(fc, "on_selection") 探测 ,实测那样查是 False,但绑定是有效的。
多页面:ScreenManager
1 2 3 4 5 6 7 8 from kivy.uix.screenmanager import ScreenManager, Screen, FadeTransitionsm = ScreenManager(transition=FadeTransition(duration=0.1 )) s1 = Screen(name="login" ) s2 = Screen(name="main" ) sm.add_widget(s1) sm.add_widget(s2) sm.current = "main"
1 2 初始 current = login 屏幕列表 = ['login', 'main'] 切换后 = main 上一屏还在吗 = True 过渡 = FadeTransition
第一个加进去的 Screen 是初始页;切走的页面不会被销毁 (has_screen 仍为 True),所以状态是保留的,这也是 Kivy 做「登录页 → 主页」的标准做法。
Qt 里对应的是 QStackedWidget,但 Kivy 的 ScreenManager 自带过渡动画。
图片与纹理
1 2 3 4 5 from kivy.uix.image import Imageim = Image(source="kivy_font_msyh_cn.png" ) Clock.tick() print (im.texture.size)
1 Image 控件 texture = (288, 43) 是否加载成功 = True
Image 是纯显示,AsyncImage 才支持 URL 异步加载;Kivy 内部所有渲染都是纹理,所以大图会占显存,实测 Texture max size = 32768。
打包发布
同样用 PyInstaller 6.22.3 实测。Kivy 这边有一个必须先知道的构建坑 ,我先踩为敬。
--collect-all kivy 会直接失败
1 python -m PyInstaller --noconfirm --onefile --windowed --name note_kivy --collect-all kivy gui_note_kivy.py
1 2 3 4 RuntimeError: Child process call to _collect_submodules() failed with: File "...\PyInstaller\utils\hooks\__init__.py", line 692, in _collect_submodules for importer, name, ispkg in pkgutil.iter_modules(paths, name + '.'): ValueError: path must be None or list of paths to look for modules in
实测数据
1 2 3 4 5 6 7 onefile 构建 32.3 秒 产物 28.7 MB onefile 构建 34.5 秒 --windowed 产物 28.7 MB(体积一样) onedir 构建 28.5 秒 目录 57.2 MB(272 个文件),主 exe 7.2 MB 跑打包出来的 exe(参数 selftest,退出码都是 0): Kivy onefile 第1次 2.1 秒 第2次 1.9 秒 (程序内部 0.83 / 0.70 秒) Kivy onedir 第1次 1.1 秒 第2次 0.9 秒 (内部 0.72 / 0.62 秒)
结论和 Qt 那边一致:onefile 启动慢一倍多 (2.0 秒 vs 1.0 秒),因为每次都要解压;--windowed 不改变体积。
Kivy 的依赖 DLL 不在 site-packages 里,实测在环境根目录:
1 2 3 kivy-env\share\angle\bin\libEGL.dll kivy-env\share\glew\bin\glew32.dll kivy-env\share\sdl2\bin\SDL2.dll、SDL2_image.dll、SDL2_mixer.dll、SDL2_ttf.dll
这些被 Kivy 的 hook 自动收进了 onedir(272 个文件里就有它们),所以不要自己乱加 --collect-all ;真要手工补,用 --add-data "…\share\sdl2\bin\*.dll;." 这种形式。
打完包一定要双击跑一次 :Kivy 的窗口依赖 OpenGL,某些机器上打包后 glew/angle 缺失会白屏,而开发环境里是好的。
移动端
Kivy 的卖点就在这儿:同一份代码用 buildozer (Android,需要 Linux)或 kivy-ios (iOS,需要 macOS)打包成 APK/IPA。本篇在 Windows 上没法实测这两条链路,只把桌面部分跑通了,移动端请按官方文档来。
桌面 GUI 里能顺手做移动端的,基本只有 Kivy 和 Flet(Flutter 那一路)。
三个框架横向对账
常见报错与调试
pip install Kivy 在 Python 3.14 上直接失败
1 2 ERROR: Could not find a version that satisfies the requirement kivy_deps.sdl2_dev~=0.7.0 (from versions: 0.8.1) ERROR: Failed to build 'Kivy' when installing build dependencies for kivy
没有 cp314 wheel,pip 退回源码构建,而它的构建依赖版本约束已经无解。换 Python 3.12/3.13 是唯一干净的办法(本篇用 3.12.16)。
传了自己的命令行参数,程序打印一堆 usage 就退出
1 2 python app.py --selftest -> -h, --help / -d, --debug / --size=640x480 / --dpi=96 ...(Kivy 的帮助)
Kivy 在 import 时解析 sys.argv,认不出的选项就当错误参数处理。自定义开关要在 import kivy 之前从 sys.argv 里摘掉。
中文全部显示成「□」方块
1 2 默认 Roboto:9 个汉字纹理宽 126px(每字 14px),字形是 notdef 方块 注册微软雅黑:288px(每字 32px),正常汉字
必须 LabelBase.register(fn_regular=...) 指一个含 CJK 的字体文件,再在控件上设 font_name。
TypeError: Cannot create graphics instruction outside the main Kivy thread
1 2 子线程里直接 add_widget -> TypeError: Cannot create graphics instruction outside the main Kivy thread 子线程里 Clock.schedule_once -> 回调线程号与主线程一致 = True
Kivy 对线程的限制是硬拦的(比 Qt 更严),所有跨线程的 UI 操作都要 Clock.schedule_once。
halign / valign 设了没反应
is 比较控件永远不相等
1 2 3 通过 KV 暴露的对象类型 = WeakProxy p.label_a is p.children[0] = False isinstance(p.label_a, Label) = True
KV 里 xxx: id 暴露出来的是弱代理,别用 is 比较,也别长期持有。
改了 ListProperty 里的元素,绑定没触发
KProperty 只在整体赋值 时发事件,self.items.append(x) 不会;要写 self.items = self.items + [x]。
动画数值对不上、测试断言随机失败
1 动画 x 采样前 5 = [0, 30.6, 62.2, 93.3, 124.7]
Kivy 的动画按帧推进,采样点取决于当时的帧间隔;测试里要断言「终值」和「单调性」,不要断言中间值。
界面一片黑、控件像浮在黑底上
1 Window.clearcolor = (0, 0, 0, 1) # 默认就是纯黑
不是 bug,是默认值。想亮色就 Window.clearcolor = (0.95, 0.95, 0.95, 1),同时注意 Label 的默认文字颜色是白的,底色改浅要一并改 color。
调试技巧
1 2 3 import osos.environ["KIVY_NO_CONSOLELOG" ] = "1" import kivy
1 2 3 不设:import kivy 时 stdout 打 11 行 [INFO]/[WARNING] 设了:只剩自己 print 的那一行 日志文件位置实测:C:\Users\Loong\.kivy\logs\kivy_26-11-04_5.txt
出错时先看这个日志文件,Kivy 会把 provider 选择、OpenGL 版本、异常都写进去。
无窗口跑测试的话,Clock.tick() 手动推帧比 app.run() 好用(本篇自检就是这么做的)。
参考