在开发 UI 的时候,我们会用到各种各样的组件来丰富 UI 的交互体验,例如:文本输入框、滚动条、 进度条、单选框等,这些组件虽然有着不同的数据、方法、视觉效果和交互方式,但都能在 LCUI 中以相同的规则来工作,而这个规则就是原型。
原型方法
原型中记录了 LCUI 在更新和渲染组件时需要用到的方法,通过将这些方法与自定义函数绑定,可实现对组件的扩展。关于原型的定义,你可以在 include/LCUI/gui/widget_base.h 中找到。
接下来让我们深入了解原型中的各个方法的用途。
init
LCUIWidget_New()
函数在找到原型后,会调用 init()
函数按照该类型的组件预设的方法初始化组件实例,不同类型的组件都会有自己的数据以及其它相关的设置,这些数据可以在 init()
函数中初始化。
destroy
在组件被销毁时会调用 destroy()
函数,通常这个函数主要负责销毁组件的私有数据、解除相关设置等。
update
对于某些组件而言,预置的 CSS 属性无法满足需求,会需要用到扩展 CSS 属性,而这些扩展 CSS 属性的处理方法是 LCUI 无法知道的,因此,LCUI 在处理完预置的 CSS 属性后,会将剩余的 CSS 属性交给 update()
函数去处理。
runtask
在 LCUI 处理完组件预设的一些任务后,会调用 runtask()
去处理组件自己设定的一 些任务。
setattr
在 Widget_SetAttribute()
设置完属性后会调用它。
settext
当解析到 XML 文档元素内的文本结点时,会调用该函数让组件处理文本内容。
autosize
在计算组件宽高时,如果组件的宽高被设置为 auto,则会调用 autosize()
获取该组件的尺寸,如果未设置 autosize
,LCUI 会按照默认的方式计算组件的宽高。通常像文本显示(TextView)这类有自己内容的组件会需要这个函数来调整自身宽高以 适应文本内容。
resize
在组件宽高更新时调用。
paint
在 LCUI 按设定的样式绘制好组件后,会调用 paint()
绘制组件自己的内容。
proto
父级原型,用于访问父级原型的方法,这个属性不需要手动设置。
创建原型
创建原型需要用到 LCUIWidget_NewPrototype()
函数:
LCUI_WidgetPrototype LCUIWidget_NewPrototype(const char *name, const char *parent_name);
它的参数有两个:原型的名称、继承的父级原型的名称,创建完后会返回原型,如果已经存在同名的原型或者原型添加失败,则会返回 NULL。
以下代码展示了原型的创建方法,如需详尽的参考代码请查阅 LCUI 预置组件的代码(例如:src/gui/widget/textview.c)。
#include <LCUI.h>
#include <LCUI/gui/widget.h>
static struct MyWidgetModule {
LCUI_WidgetPrototype prototype;
// 其它用得到的数据
// xxxx
// ...
} my_widget;
static void MyWidget_OnInit(LCUI_Widget w)
{
// 初始化一些数据
}
static void MyWidget_OnDestroy(LCUI_Widget w)
{
// 释放相关数据
}
static void MyWidget_UpdateStyle(LCUI_Widget w)
{
// 处理扩展的样式属性
}
static void MyWidget_AutoSize(LCUI_Widget w, float *width, float *height)
{
// 根据自身的内容,计算合适的尺寸
}
static void MyWidget_OnTask(LCUI_Widget w)
{
// 处理积累的任务
}
static void MyWidget_OnPaint(LCUI_Widget w, LCUI_PaintContext paint)
{
// 利用 paint 上下文绘制自己的内容
}
static void MyWidget_OnParseText(LCUI_Widget w, const char *text)
{
// 处理 XML 解析器传来的文本内容
}
void LCUIWidget_AddMyWidget(void)
{
int i;
my_widget.prototype = LCUIWidget_NewPrototype("my-widget", NULL);
my_widget.prototype->init = MyWidget_OnInit;
my_widget.prototype->paint = MyWidget_OnPaint;
my_widget.prototype->destroy = MyWidget_OnDestroy;
my_widget.prototype->autosize = MyWidget_AutoSize;
my_widget.prototype->update = MyWidget_UpdateStyle;
my_widget.prototype->settext = MyWidget_OnParseText;
my_widget.prototype->runtask = MyWidget_OnTask;
// 如果需要用到全局的数据的话
// my_widget.xxxx = ???
// ...
}
使用私有数据
当组件的功能变多变复杂的时候,如果仅靠函数内的局部变量难以实现多个功能之间的数据共享的话,那么就会需要一个能在整个组件生命周期内有效的空间来存放数据,例如:文本编辑框,它会保存当前编辑的文本内容,调用相关函数可以对这个文本内容进行读写操作,在绘制时也会需要用到这些文本内容以在屏幕上绘制出相应的文字。
组件私有数据的操作函数有以下两个:
void *Widget_AddData(LCUI_Widget widget, LCUI_WidgetPrototype proto, size_t data_size);
void *Widget_GetData(LCUI_Widget widget, LCUI_WidgetPrototype proto);
从以上代码中可以看出组件私有数据有添加和获取这两种方法,私有数据是与组件原型绑定的,添加时需要指定具体的内存占用大小。添加后,可以调用 Widget_GetData()
函数获取私有数据,这个函数也同样需要指定原型。
以下是这两个函数的基本用法示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <LCUI.h>
#include <LCUI/gui/widget.h>
/** 组件私有数据的结构 */
typedef struct MyWidgetRec_ {
int a;
char b;
double c;
char *str;
} MyWidgetRec, *MyWidget;
static struct MyWidgetModule {
LCUI_WidgetPrototype prototype;
// 其它用得到的数据
// xxxx
// ...
} self;
static void MyWidget_OnInit(LCUI_Widget w)
{
MyWidget data;
const size_t size = sizeof(MyWidgetRec);
data = Widget_AddData(w, self.prototype, size);
// 初始化私有数据
data->a = 123;
data->b = 'b';
data->c = 3.1415926;
data->str = malloc(256 * sizeof(char));
strcpy(data->str, "this is my widget.");
printf("my widget is inited.\n");
}
static void MyWidget_OnDestroy(LCUI_Widget w)
{
MyWidget data = Widget_GetData(w, self.prototype);
// 释放私有数据占用的内存资源
free(data->str);
printf("my widget is destroied.\n");
}
void LCUIWidget_AddMyWidget(void)
{
int i;
self.prototype = LCUIWidget_NewPrototype("mywidget", NULL);
self.prototype->init = MyWidget_OnInit;
self.prototype->destroy = MyWidget_OnDestroy;
// 如果全局用得到的数据的话
// self.xxxx = ???
}
使用 CLI 添加组件
LCUI CLI 提供了组件生成器,你可以基于它生成的组件模板代码快速开发组件。
lcui generate widget 你的组件名
继承
在某一个组件的功能不够用的时候,我们会想基于它扩展一些新功能,如果直接改代码的话会让它容易变得更复杂,而重新写一个成本太高,所以,这个时候我们可以使用原型的“继承”功能来创建一个组件的扩展版本, 即能保留原组件的功能,又能使用新加的功能。
以 TextView 组件为例,假设有这么个需求:能够支持设置网页链接,在组件被点击时调用浏览器打开这个链接,网页链接由 href
属性提供,以下是示例代码:
#include <string.h>
#include <stdlib.h>
#include <LCUI.h>
#include <LCUI/gui/widget.h>
typedef struct LinkRec_ {
char *href;
} LinkedRec, *Link;
LCUI_WidgetPrototype prototype;
static void Link_OnClick(LCUI_Widget w, LCUI_WidgetEvent e, void *arg)
{
Link link = Widget_GetData(w, prototype);
if (link->href) {
// 调用浏览器打开链接
// ...
}
}
static void Link_OnInit(LCUI_Widget w)
{
const size_t size = sizeof(LinkRec);
Link link = Widget_AddData(w, prototype, size);
link->href = NULL;
Widget_BindEvent(w, "click", Link_OnClick, NULL, NULL);
// 调用父级原型的 init() 方法,继承父级现有的功能
// 在其它语言中(例如 JavaScript),这段代码类似于在构造函数中调用 super()
prototype->proto->init(w);
}
static void Link_OnDestroy(LCUI_Widget w)
{
Link link = Widget_GetData(w, prototype);
free(link->href);
prototype->proto->destroy(w);
}
void Link_SetHref(LCUI_Widget w, const char *href)
{
Link link = Widget_GetData(w, prototype);
if (link->href) {
free(link->href);
link->href = NULL;
}
if (href) {
size_t len = strlen(href) + 1;
link->href = malloc(len * sizeof(char));
strcpy(link->href, href);
}
}
static void Link_OnSetAttr(LCUI_Widget w, const char *name, const char *value)
{
// 响应 href 属性值变化
if (strcmp(name, "href") == 0) {
Link_SetHref( w, value );
}
}
void LCUIWidget_AddLink( void )
{
// 创建一个名为 link 的原型,继承自 textview
prototype = LCUIWidget_NewPrototype("link", "textview");
prototype->init = Link_OnInit;
prototype->destroy = Link_OnDestroy;
prototype->setattr = Link_OnSetAttr;
}
以上代码创建了一个名为 link 的原型,接下来将展示如何使用它:
...
LCUIWidget_AddLink();
...
LCUI_Widget link = LCUIWidget_New("link");
Link_SetHref(link, "https://www.example.com");
<?xml version="1.0" encoding="UTF-8" ?>
<lcui-app>
<ui>
<link href="https://www.example.com">点击这里</link>
</ui>
</lcui-app>
LCUI 的预置组件 Anchor 是这个示例组件的完整实现,如需了解更多,可查看文件:src/gui/widget/anchor.c