抽象层¶
Charmy 采用 三层架构 ,从下到上依次为:
后端层 (Backend Layer) — SDL2 + Cairo (Genesis)
图形层 (Graphics Layer) — DrawnShape, DrawnLine, DrawnText…
控件层 (Widget Layer) — Button, Window, Frame…
为什么需要抽象层¶
不同的操作系统和图形库有不同的 API。为了让 Charmy 能 跨平台 运行, 并且能够在不同后端之间切换,我们需要将后端的具体实现抽象出来。
后端模块的划分¶
charmy/backend/ 目录下的模块:
模块 |
作用 |
|---|---|
|
定义后端接口模板(抽象基类) |
|
Genesis 后端的具体实现(SDL2 + Cairo) |
|
后端加载器,根据名称动态加载后端 |
|
后端工具模块,安全引用 Charmy 上层对象,避免循环引入 |
后端模板 (template.py)¶
在 charmy/backend/template.py 中定义了所有后端的接口模板:
class Backend:
name: str # 后端标识名
friendly_name: str # 可读名称
WindowBase: type[WindowBase] # 窗口操作
LineBase: type[LineBase] # 线条绘制
ShapeBase: type[ShapeBase] # 形状绘制
TextureBase: type[TextureBase] # 纹理处理
TextBase: type[TextBase] # 文字绘制
每个 *Base 类都有一个 supports 属性,声明后端支持哪些特性:
class WindowSupportState(SupportState):
set_title: bool = False # 是否支持修改标题
set_icon: bool = False # 是否支持修改图标
set_pos: bool = False # 是否支持移动窗口
set_size: bool = False # 是否支持调整大小
transparent: bool = False # 是否支持透明度
fullscreen: bool = False # 是否支持全屏
这种设计 allows Charmy 在运行时查询后端能力,选择最优的绘制方式。
警告
后端模板中使用了一个**循环引用设计**:
class WhateverBase:
Backend: type[Backend] = Backend # 引用 Backend 类
每个 *Base 类持有 Backend 类引用,而 Backend 类又持有
各个 *Base 类引用。这是为了方便后端内部调度,但会导致
Sphinx autodoc 在生成文档时出现递归深度溢出。
解决方案见 docs/source/conf.py 中的 _skip_circular_refs 函数。
Genesis 后端¶
目前唯一的后端实现,使用 SDL2 创建和管理窗口,使用 Cairo 进行 2D 渲染。这一后端为Charmy的上层部分提供一套 可用于可行性验证的API,而并非一个合格且正式的后端选项。您不应该在生产环境内使用它。
SDL2 负责:
窗口创建与管理
事件循环(鼠标、键盘、窗口事件)
像素缓冲区管理(SDL Surface)
窗口刷新 (SDL_UpdateWindowSurface)
Cairo 负责:
矢量图形渲染(线条、形状、贝塞尔曲线)
文字渲染
图像合成
备注
Genesis 后端的一个设计特点是:Cairo 绘制到内存中的 ImageSurface,
然后通过 ctypes.memmove 直接拷贝到 SDL2 的窗口 Surface。
这种方式避免了额外的编码/解码开销,性能较好。
这一特性是为了拯救Genesis后端那本就不充裕的性能。
事件桥接¶
Genesis 后端通过 sdl2_handle_event() 方法将 SDL2 的事件转换为 Charmy 事件:
# genesis.py (简化)
def sdl2_handle_event(self, event):
match event.type:
case sdl2.SDL_MOUSEMOTION:
self.charmy_window.trigger(MouseMove(...))
case sdl2.SDL_MOUSEBUTTONDOWN:
self.charmy_window.trigger(MousePress(...))
case sdl2.SDL_MOUSEBUTTONUP:
self.charmy_window.trigger(MouseRelease(...))
case sdl2.SDL_WINDOWEVENT:
# 处理窗口大小变化、移动、焦点事件
...
case sdl2.SDL_QUIT:
self.charmy_window.destroy()
渲染流程¶
完整的帧渲染流程:
mainloop() 每帧调用
│
▼
Window.update()
│
├─ Widget.draw() → 控件更新图形对象
│ ├─ Button: 更新 DrawnShape + DrawnText
│ └─ Container: 递归绘制所有子控件
│
├─ Window.draw_frame()
│ ├─ 绘制背景
│ ├─ 绘制所有 DrawnLine
│ ├─ 绘制所有 DrawnShape
│ └─ 绘制所有 DrawnText
│
├─ Cairo 渲染到 ImageSurface
│
├─ ctypes.memmove → SDL2 Surface
│
└─ SDL_UpdateWindowSurface → 屏幕显示
Fallback 机制¶
Charmy 的线条和形状系统支持 fallback ——如果后端不支持某种图形类型, 会自动降级为其他类型绘制。
例如,Genesis 后端支持 CircleArc (圆弧)的直接绘制,
但如果某个假想的后端只支持 CubicBezier (三次贝塞尔曲线),
Charmy 会自动将圆弧分解为多条贝塞尔曲线:
# 用户代码(不关心后端是否支持)
arc = cm.styles.shape.CircleArc(center=(100, 100), radius=50,
start_orient=0, end_orient=270)
cm.graphics.DrawnLine(window, arc, (255, 0, 0)).draw()
Fallback 链示例:
CircleArc → 不支持 → CubicBezier(s) → 还不支持? → PolyLine(s)
↓
Line(s)
↓
警告:无法绘制
这种机制于 shape.py 中每个线条类的 fallback() 方法中实现:
class CircleArc(Curve):
def fallback(self, _from=[]):
# 转换为多次贝塞尔曲线
return [CubicBezier(...), CubicBezier(...), ...]
小技巧
如果您是一位足够偷懒的后端开发者,您只需要确保后端支持绘制直线和三次贝塞尔曲线,就可以透过此fallback机制绘制 Charmy中的任何线条了。
更多细节请参考 charmy/backend/ 目录下的源代码和 tests/ 目录下的测试文件。