最近打算用C写个东西,想找个轻量的UI库,想起以前star过的一个repo,只有一个头文件约15000行的ANSI C代码,没有任何标准库以外的依赖。就是这个:https://github.com/vurtun/nuklear 。嗯,Nuklear,核动力。
看效果真心不错,让我想起另一个UI库zebkit,HTML5 canvas的。事实上这两个还真有点像,就是都是命令式绘制的。本来想照着demo用SDL做一个简单的界面出来,结果久别重逢C语言,一不小心把源码review了一遍(张全蛋:silly B stupid Chinglish。不不不,只是觉得说代码审查不对味儿...)。下面是简单的分析。
Nuklear用一个nk_context结构维护输入,风格,内存,剪贴板,命令队列,鼠标合成,窗口管理。对于一个nk_context实例,基本渲染流程为:
-
应用程序初始化后开始如下帧循环逻辑。
- 应用程序从平台相关接口获取输入事件,根据事件类型对nk_context调用nk_input_*等api,Nuklear将各类事件参数记录在nk_context中。注意前后要调用nk_input_begin和nk_input_end以初始化和再处理一些数据。
- 应用程序对nk_context调用nk_begin绘制窗口,Nuklear用窗口名和标题计算hash并在nk_context的窗口链表中查找,如果窗口不存在则创建并加入链表,否则只更新窗口的参数(如位置大小样式等)。同时,Nuklear还会在此时根据上一步骤中记录的输入事件处理窗口响应行为。
- 应用程序调用nk_layout_*、nk_button_*等api绘制布局和控件,Nuklear判断控件可见性并“绘制”控件,这里的绘制实际上是向nk_context的命令队列插入绘制命令。同时,Nuklear根据上一步骤中记录的输入事件处理控件响应行为。
- 应用程序对nk_context调用nk_end结束绘制窗口。
- 应用程序对nk_context调用nk_foreach宏,循环取出上一步骤中“绘制”控件时加入的绘制命令,并根据命令类型调用平台相关接口进行实际的绘制。
- 开始下一帧的循环。
总结起来,这个库实现轻巧,代码也用了一些技巧,看起来很干净。以个人的看法评价下优缺点:
首先,整个UI的驱动机制,从事件到渲染都是命令式的,nk_context只管理窗口并不维护控件的数据结构,应用程序需要自己维护数据,在绘制控件时传入。这样的好处是内存占用小,数据甚至可以只存在于栈中,而且命令式渲染可以很方便灵活地定制效果。
但是,控件的绘制和响应输入事件耦合在了一起,缺少独立数据结构导致应用程序需要协助维护控件状态,并在绘制控件时通过nk_context传入,比如last_widget_state表示控件一系列响应鼠标等输入事件的状态,这些本应该是应用程序不关心的。
其次,命令式绘制导致每帧都需要重复绘制,即使界面并没有发生变化。在源码开头的注释中提到定义NK_ZERO_COMMAND_MEMORY的宏可以在加入绘制命令到队列之前memset清零命令结构体,从而利用memcmp快速检查命令队列是否变化从而避免实际绘制(不清零会有随机数据无法比较)。然而找遍了源码也没有找到实际的使用,也没有提供相关的utils,如果需要自己实现的话感觉有点侵入式了。
最后,不得不说乍一看single header file的ANSI C代码似乎很好看,但是细看发现一些符号命名真是随意,创建窗口的api是nk_begin,源码中还有nk__begin(两个下划线),nk_start,nk_draw_begin,nk__draw_begin,虽然明白意图是私有符号,但是阅读起来真容易马虎。还有api动词名词就不说了,但是大括号换行和不换行两种风格夹杂,赋值语句对齐和不对齐交替,if语句不加花括号一会写在一行一会写两行。
自从当兵几年没用C还以为都忘干净了,重新拾起来吧。