跳到主要内容
版本:2.x

原型

在开发 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