Kivy 绘制入门:深入理解 Widget 的 Canvas、绘制指令与自动重绘机制
2026/9/20 16:15:48 网站建设 项目流程

Kivy 绘制入门:深入理解 Widget 的 Canvas、绘制指令与自动重绘机制

【免费下载链接】kivyOpen source UI framework written in Python, running on Windows, Linux, macOS, Android and iOS项目地址: https://gitcode.com/gh_mirrors/ki/kivy

Kivy 中每个 Widget 都自带一个Canvas(画布),它是所有自定义图形绘制的核心入口。本文以 Kivy 官方入门指南中的 Drawing 章节为主体,结合本仓库源码(kivy/graphics/ 与 kivy/uix/widget.py)深入讲解:Canvas 是什么、context 指令与 vertex 指令的区别、如何通过 Python 与 kv 语言两种方式添加指令、如何利用canvas.before/canvas.after控制绘制时机,以及 kv 声明式语法为何能在属性变化时自动重绘。读完本文,你将能独立为任意自定义 Widget 编写声明式或命令式的图形绘制代码,并理解其底层执行机制。

每个 Widget 都有一块属于自己的画布

Kivy 的设计哲学是"关注点分离":Widget 负责逻辑与状态,而图形表示(graphical representation)由独立的 Canvas 承载。正如 kivy/uix/widget.py 中的类文档所写:

Widgets don't have adraw()method. ... Every widget has its own Canvas that you can use to draw.

也就是说,Kivy 的 Widget没有draw()方法,每个 Widget 实例都拥有一个独立的Canvas对象。这带来两个直接后果:

  1. 你可以在 Widget 类外部自由定制它的图形表示,而不必继承改写绘制逻辑;
  2. 所有绘制指令与 Widget 的状态(如possize)分离存储,Kivy 可以高效地批量渲染。

在源码层面,Widget 初始化时会自动创建默认画布(kivy/uix/widget.py):

# Create the default canvas if it does not exist. if self.canvas is None: self.canvas = Canvas(opacity=self.opacity)

canvas属性即 Widget 的默认画布(kivy/uix/widget.py)。注意:Kivy 刻意不为 Widget 提供"背景色"之类的通用属性,保持设计精简——自定义 Widget 的图形表示完全由开发者通过 canvas 指令从零构建,这可以从 Button 等派生类的实现模式中得到印证。

Canvas 的本质:一组按序执行的绘制指令

从 kivy/graphics/instructions.pyx 中Canvas类的定义来看:

The important Canvas class. Use this class to add graphics or context instructions that you want to be used for drawing.

Canvas 并不是一个位图缓冲,而是一组(group of)绘制指令的容器。当 Widget 的图形表示需要更新时,Kivy 会按指令在画布中的先后顺序依次执行它们。官方入门文档(Drawing 章节)对此的表述是:

The canvas is a group of drawing instructions that should be executed whenever there is a change to the widget's graphical representation.

指令的执行顺序直接决定渲染结果:后加入的指令绘制在先前指令之上。源码中Canvas.add的实现保证了这一点(kivy/graphics/instructions.pyx)——若画布中存在after组,新指令会被插入到after组之前,使after组始终保持在最末绘制。

两种指令类型:Context 指令与 Vertex 指令

Kivy 的绘制指令分为两大类,官方文档明确给出了这一划分:

指令类型作用典型指令源码位置
context 指令修改绘制状态(颜色、变换矩阵、纹理绑定等),本身不产生图形,只影响其后的顶点指令ColorPushMatrixPopMatrixTranslateRotateScaleBindTexturekivy/graphics/context_instructions.pyx
vertex 指令真正提交几何数据(顶点、纹理坐标)到 GPU 进行绘制RectangleEllipseLineQuadTriangleMeshPointBezierkivy/graphics/vertex_instructions.pyx

两类指令的基类分别是ContextInstructionVertexInstruction,二者共同继承自最底层的Instruction("the smallest instruction available",见 kivy/graphics/instructions.pyx)。所有常用指令都从 kivy/graphics/__init__.py 统一导出,因此你通常只需写一行导入:

from kivy.graphics import Color, Rectangle, Ellipse, Line, Translate, Rotate, Scale

Context 指令实例:Color 是"乘法器"而非"画笔"

Color是最常用的 context 指令。它的语义很微妙:它不是设置画笔颜色,而是作为乘数(multiplier)作用于其后所有顶点指令的纹理颜色。源码文档对此有精确描述(kivy/graphics/context_instructions.pyx):

This represents a color between 0 and 1, but is applied as a multiplier to the texture of any vertex instructions following it in a canvas.

例如,若某Rectangle使用了一张均匀颜色为(0.5, 0.5, 0.5, 1.0)的纹理,而其前有一条Color(rgba=(1, 0.5, 2, 1)),则实际可见颜色为(0.5, 0.25, 1.0, 1.0)——蓝色分量因乘数2而翻倍,超出 0–1 范围在乘数语义下是合法的。

Color支持多种构造方式(源码中 kivy/graphics/context_instructions.pyx 有完整示例):

from kivy.graphics import Color # 红色、绿色 c = Color(1, 0, 0) c = Color(0, 1, 0) # 绿色 + 50% 透明度 c = Color(0, 1, 0, .5) # HSV 模式 c = Color(0, 1, 1, mode='hsv') c = Color(0, 1, 1, .2, mode='hsv') # 仅设置某个分量 c = Color(b=0.5)

对应的 kv 写法(属性名rgb/rgba/hsv+a):

<Rule>: canvas: Color: rgb: 1, 0, 0 Color: rgba: 0, 1, 0, .5 Color: hsv: 0, 1, 1 a: .5

Vertex 指令实例:Rectangle 与 Ellipse

Rectangle是绘制矩形的基础 vertex 指令,构造参数为possize(kivy/graphics/vertex_instructions.pyx):

from kivy.graphics import Rectangle # 不传参时默认 pos=(0, 0)、size=(100, 100) r = Rectangle(pos=(10, 20), size=(200, 150))

Ellipse继承自Rectangle(kivy/graphics/vertex_instructions.pyx),参数完全相同,绘制的是矩形包围盒内的椭圆/圆。源码中Rectangle.build()(kivy/graphics/vertex_instructions.pyx)展示了其底层工作方式:把(x, y, w, h)展开为 4 个顶点坐标,连同 6 个索引(两个三角形)交给batch.set_data()提交渲染——这正是 Canvas"按需执行指令"的底层体现。

两种添加指令的方式:Python 与 kv 语言

官方入门文档明确指出,指令可以从 Python 代码从 kv 文件(推荐方式)添加。两种方式的能力完全等价,区别在于更新机制。

方式一:Python 代码 + 上下文管理器

在 Python 中,最优雅的写法是利用with语句。Canvas支持 Python 的上下文管理器协议——CanvasBase实现了__enter__/__exit__(kivy/graphics/instructions.pyx),进入时通过pushActiveCanvas把当前画布压入内部栈,退出时popActiveCanvas恢复(kivy/graphics/instructions.pyx),因此with块内创建的每条指令会自动挂到该画布上:

from kivy.graphics import Color, Rectangle with self.canvas: Color(1., 1., 0) Rectangle(size=(50, 50))

不使用with的等价写法是显式调用add(源码中两种用法均有示例,见 kivy/graphics/instructions.pyx):

self.canvas.add(Color(1., 1., 0)) self.canvas.add(Rectangle(size=(50, 50)))

方式二:kv 语言(推荐)

kv 文件中的canvas:块是声明式绘制的核心语法。官方文档推荐此方式,因为在 kv 中,当指令所依赖的属性(如self.posself.size)发生变化时,相关指令会被自动标记为需要更新并重绘;而在 Python 中,这个联动需要你自己维护。

<MyWidget>: canvas: Color: rgba: 0.5, 0.5, 0.5, 0.5 Ellipse: pos: self.pos size: self.size

从解析器源码可以看到,kv 语言对canvascanvas.aftercanvas.before三种键有专门的语法分支(kivy/lang/parser.py),它们分别被挂载到对象的canvas_rootcanvas_beforecanvas_after规则上,随后由 kivy/lang/builder.py 翻译成真实的 Canvas 指令树。

Python 侧的手动重绘:以官方示例为例

上述 kv 片段对应的纯 Python 实现(即官方文档配套图片gs-drawing.png左侧的代码)大致如下:

from kivy.app import App from kivy.graphics import Color, Ellipse from kivy.uix.widget import Widget class MyWidget(Widget): def __init__(self, **kwargs): super(MyWidget, self).__init__(**kwargs) # 手动绑定:pos / size 变化时重绘画布 self.bind(pos=self.update_canvas, size=self.update_canvas) def update_canvas(self, *args): self.canvas.clear() with self.canvas: Color(0.5, 0.5, 0.5, 0.5) Ellipse(pos=self.pos, size=self.size) class MyApp(App): def build(self): return MyWidget() if __name__ == '__main__': MyApp().run()

注意这里的self.bind(pos=..., size=...)self.canvas.clear()正是官方文档强调的差异点:在 Python 中"你需要自己完成"属性变化与重绘的联动。而在 kv 中,Ellipse(pos=self.pos, size=self.size)会让 Kivy 自动建立这种依赖关系,possize一变,画布自动重绘。

两种方式下,只要MyWidgetpositionsize发生变化,其 canvas 都会被重新绘制——kv 靠自动绑定,Python 靠显式绑定。

用 canvas.before 与 canvas.after 控制绘制时机

官方文档指出,可以使用canvas.beforecanvas.after分组,按执行时机分离指令:

  • canvas.before:在默认画布内容之前执行——适合绘制背景、边框等"垫底"图形;
  • canvas(默认画布):常规指令,按添加顺序执行;
  • canvas.after:在默认画布内容之后执行——适合绘制前景、覆盖层、描边等"置顶"图形。

源码中before/after是惰性创建的属性(kivy/graphics/instructions.pyx):首次访问before时,会创建一个CanvasBase组并insert(0, ...)插入到画布最前面;首次访问after时,则通过add追加到画布最后面Canvas.add的实现(kivy/graphics/instructions.pyx)专门保证了after组永远保持在末尾,任何后加的指令都会被插到它前面。

kv 中写法完全一致:

<MyWidget>: canvas.before: # 背景:先绘制 Color: rgba: 0.2, 0.2, 0.2, 1 Rectangle: pos: self.pos size: self.size canvas: # 主体内容 Ellipse: pos: self.pos size: self.size canvas.after: # 描边:最后绘制,覆盖在最上层 Color: rgba: 1, 1, 1, 1 Line: rectangle: self.pos + self.size

仓库中的真实示例 examples/widgets/colorusage.py 大量使用了canvas.before为 Label 绘制彩色背景,并对比了rgbrgbahsv以及hex('#27ae60')四种配色写法,是理解分组与配色语法的绝佳参考:

<Root>: cols: 2 canvas: Color: rgba: 1, 1, 1, 1 Rectangle: pos: self.pos size: self.size Label: canvas.before: Color: rgb: 39/255., 174/255., 96/255. Rectangle: pos: self.pos size: self.size text: "rgb: 39/255., 174/255., 96/255."

此外,Widget.add_widget也支持通过canvas参数('before''after'None)指定子 Widget 的画布挂载位置(kivy/uix/widget.py),可用来控制系统级的绘制层级。

Canvas 的常用方法:clear 与 ask_update

除了add/remove/insert之外,两个高频方法值得掌握(kivy/graphics/instructions.pyx):

  • canvas.clear():清空画布上所有指令(保留before/after组)。Python 手动重绘的典型模式就是"先 clear 再重建";
  • canvas.ask_update():通知画布在下一帧重绘。当你因某个外部值(非pos/size)变化而需要触发重绘时使用。

另外,Canvas还提供opacity属性控制整个画布的透明度。它是一个累积属性(kivy/graphics/instructions.pyx):父子画布透明度相乘,例如父画布 opacity=0.5、子画布 opacity=0.2,则实际透明度为0.5 * 0.2 = 0.1。Widget 的opacity属性变化时,其 canvas 的 opacity 也会同步更新(kivy/uix/widget.py)。

指令依赖与自动更新:从源码看重绘机制

官方文档强调 kv 方式的优势在于"指令依赖的属性变化时自动更新"。这一机制在源码中的落点是Instruction.flag_update(kivy/graphics/instructions.pyx):指令被标记为GI_NEEDS_UPDATE,并向上递归标记父组,最终通知所属 Canvas 在下一帧重绘。kv 中Ellipse(pos=self.pos, size=self.size)会让 Kivy 为Ellipse.pos/Ellipse.size与 Widget 的pos/size建立表达式绑定,任一属性变化即触发上述标记链——这就是"kv 自动更新、Python 手动绑定"的底层差异。

顺带一提,Kivy 对图形指令的创建与修改有线程约束:源码在非主线程创建或修改指令时会抛出TypeError(kivy/graphics/instructions.pyx),因此所有绘制操作都应发生在主线程的事件循环中。

小结

回顾官方入门文档 Drawing 章节的全部要点:

  1. 每个 Widget 都有独立的canvas,它是一组按序执行的绘制指令,而非位图;
  2. 指令分为context 指令(改状态,如Color)与vertex 指令(画图形,如Rectangle/Ellipse/Line);
  3. 指令可从 Python(with self.canvas:)或 kv 文件(canvas:块)添加,kv 为推荐方式;
  4. kv 中指令依赖的属性变化时会自动重绘;Python 中需自行bindclear()重建;
  5. canvas.before/canvas.after可精确控制绘制先后顺序,实现背景、主体、前景分层。

如需深入了解 Kivy 图形系统的整体架构(着色器、FBO、变换矩阵、模板测试等),可直接阅读 kivy/graphics/init.py 与 kivy/graphics/ 目录下各模块的源码文档;想查看更多绘制实战,examples/canvas/ 目录下的lines.pycircle.pymesh.pyrounded_rectangle.py等示例覆盖了从基础图形到网格、抗锯齿的完整场景。

【免费下载链接】kivyOpen source UI framework written in Python, running on Windows, Linux, macOS, Android and iOS项目地址: https://gitcode.com/gh_mirrors/ki/kivy

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询