# 3. 开发自定义控件

# 3.1 创建自定义控件

开发自定义控件之前,需要参考 AWTK 自定义控件规范 (opens new window) 搭建自定义控件框架,为了节省开发时间,建议使用 Designer 新建自定义控件项目,新建完成后可以得到一个空白控件的框架,步骤如下图所示。

图3.1 新建自定义控件项目
图3.1 新建自定义控件项目

新建完成后,Designer 会自动打开该项目,并且可以在指定的项目路径中找到 awtk-widget-xxx 项目,例如此处的 awtk-widget-custom-widget,自定义控件项目的目录结构及其说明详见本文第一章。

# 3.2 实现自定义控件

使用 Designer 创建好的自定义控件只是一个空白的控件框架,无具体功能,需要我们遵循 AWTK 自定义控件规范 (opens new window) 完善相关的代码,才能真正实现自定义控件。下文会以 Designer 中内置推荐的 number_label 控件为例,介绍如何实现一个具体的自定义控件。

number_label 控件的获取方法详见本文第一章。

# 3.2.1 控件的虚表结构体

在编写控件的逻辑代码之前,首先需要了解 AWTK 中的控件虚表结构体(widget_vtable_t),该结构体中的成员主要描述控件类型,并完成控件的创建、绘制以及事件响应等等,详细的声明代码如下详见:widget.h (opens new window)

在实现自定义控件时,我们通常需要实现的 widget_vtable_t 结构体中的成员(属性/函数)详见下表:

属性/函数 类型 说明 触发时机
size uint32_t 控件类型的大小
type const char* 控件类型的名称
create 函数指针 控件的构造函数 创建控件
on_paint_self 函数指针 控件的绘制函数 每一帧都会调用该函数绘制控件
on_event 函数指针 控件的事件处理函数 控件接收到输入设备事件
set_prop 函数指针 设置控件属性 调用 widget_set_prop 设置属性
get_prop 函数指针 获取控件属性 调用 widget_get_prop 获取属性
on_destroy 函数指针 控件的销毁回调函数 销毁函数

除了以上列表中介绍的属性/函数外,控件虚表结构体(widget_vtable_t)中还有许多其他可重载的控件函数,具体请查看 widget.h (opens new window)

在代码中,可以直接使用 AWTK 提供的宏定义 TK_DECL_VTABLE 来创建控件的虚表结构体(widget_vtable_t),该宏定义的声明如下:

/* awtk/src/base/types_def.h */
#define TK_DECL_VTABLE(vt)                                                       \
  extern const widget_vtable_t g_##vt##_vtable;                                  \
  const widget_vtable_t* vt##_get_widget_vtable(void) {return &g_##vt##_vtable;} \
  const widget_vtable_t g_##vt##_vtable

例如,在 number_label 控件中,widget_vtable_t 的定义代码如下,下文会详细介绍这些属性和函数的实现:

/* awtk-widget-number-label/src/number_label/number_label.c */
TK_DECL_VTABLE(number_label) = {.size = sizeof(number_label_t),
                                .type = WIDGET_TYPE_NUMBER_LABEL,
                                ......
                                .create = number_label_create,
                                .on_paint_self = number_label_on_paint_self,
                                .set_prop = number_label_set_prop,
                                .get_prop = number_label_get_prop,
                                .on_event = number_label_on_event,
                                .on_destroy = number_label_on_destroy};

# 3.2.2 控件类型名称

控件类型名称要求使用小写的英文单词,多个单词之间用下划线连接。例如 number_label 控件的类型名称定义如下:

/* awtk-widget-number-label/src/number_label/number_label.h */
#define WIDGET_TYPE_NUMBER_LABEL "number_label"

并且该类型名称需要设置到控件虚表结构体(widget_vtable_t)中,让 AWTK 能够通过类型名称创建注册的控件,代码如下:

/* awtk-widget-number-label/src/number_label/number_label.c */
TK_DECL_VTABLE(number_label) = {.type = WIDGET_TYPE_NUMBER_LABEL,
                                ......};

# 3.2.3 控件类型结构体

控件类型结构体定义时要求为:控件类型名称 + "_t",并且必须以 widget_t 作为父类,例如 number_label 控件结构体的定义如下:

/* awtk-widget-number-label/src/number_label/number_label.h */
typedef struct _number_label_t {
  widget_t widget;
  ......
} number_label_t;

在控件类型结构体中可以定义控件特有的属性,属性名称要求使用小写的英文单词,多个单词之间用下划线连接,例如 number_label 控件的 format 属性定义如下:

/* awtk-widget-number-label/src/number_label/number_label.h */
typedef struct _number_label_t {
  widget_t widget;

  /**
   * @property {char*} format
   * @annotation ["set_prop","get_prop","readable","design","scriptable"]
   * 格式字符串。
   */
  char* format;
  ......
} number_label_t;

属性注释的格式以及详细含义请查阅:AWTK API 注释格式 (opens new window)

定义好控件类型结构体后,需要将其大小设置到控件虚表结构体(widget_vtable_t)中,代码如下:

/* awtk-widget-number-label/src/number_label/number_label.c */
TK_DECL_VTABLE(number_label) = {.size = sizeof(number_label_t),
                                ......};

AWTK 创建控件时,将使用控件虚表中的 size 属性 malloc 内存,如果该属性设置有误,程序运行中可能会出现越界访问,从而导致程序崩溃。

# 3.2.4 控件的构造函数

控件构造函数主要负责分配内存以及初始化控件属性,通常都需要先调用 widget_create 函数创建 widget_t 基类对象,并将控件虚表结构体(widget_vtable_t)赋给基类对象。例如 number_label 控件的构造函数代码如下:

/* awtk-widget-number-label/src/number_label/number_label.c */
widget_t* number_label_create(widget_t* parent, xy_t x, xy_t y, wh_t w, wh_t h) {
  /* 创建对象并分配内存 */
  number_label_t* number_label =
      NUMBER_LABEL(widget_create(parent, TK_REF_VTABLE(number_label), x, y, w, h));

  /* 初始化控件属性 */
  number_label->format = tk_strdup(NUMBER_LABEL_DEFAULT_FORMAT);
  number_label->min = 0;
  number_label->max = 0;
  number_label->step = 1;
  number_label->readonly = FALSE;
  number_label->decimal_font_size_scale = 0.6;

  return (widget_t*)number_label;
}

# 3.2.5 控件的绘制函数

AWTK 程序启动后,会进入 GUI 主循环线程,每一循环一次(即绘制一帧)就会调用一次各个控件的绘制函数。通常我们可以调用 AWTK 提供的 canvas 和 vgcanvas 来绘制控件,具体的用法可以参考《AWTK开发实践》中的画布章节。

例如,此处简单介绍 number_label 控件中的绘制函数:

/* awtk-widget-number-label/src/number_label/number_label.c */
static ret_t number_label_paint_text(widget_t* widget, canvas_t* c, wstr_t* text) {
  /* 获取样式控件当前使用的样式属性:比如字体颜色、格式、字号、边距等等 */
  style_t* style = widget->astyle;
  color_t tc = style_get_color(style, STYLE_ID_TEXT_COLOR, trans);
  int32_t margin = style_get_int(style, STYLE_ID_MARGIN, 0);
  ......

  /* 设置画布的字体颜色和水平、垂直布局 */
  canvas_set_text_color(c, tc);
  canvas_set_text_align(c, align_h, align_v);

  /* 设置画布使用的字体并计算文本宽度 */
  canvas_set_font(c, font_name, font_size);
  int_part_width = canvas_measure_text(c, text->str, int_part_len);

  /* 其他画布相关操作 */
  ......

  /* 绘制文本 */
  canvas_draw_text(c, text->str + int_part_len, decimal_part_len, x, y);

  return RET_OK;
}

static ret_t number_label_on_paint_self(widget_t* widget, canvas_t* c) {
  /* 前置操作 */
  ......
  /* 绘制控件文本 */
  return number_label_paint_text(widget, c, text);
}

控件绘制函数主要就是依赖 AWTK 提供的 canvas 和 vgcanvas 实现,详情可以参考《AWTK开发实践》,并且在 AWTK 的内部控件以及 Designer 推荐的自定义控件中都有大量的相关示例,均可参阅,此处便不再详细介绍了。

# 3.2.6 控件的事件处理函数

如果想让控件在收到事件时做出相应的处理,那么就需要重载控件虚表结构体(widget_vtable_t)中 on_event 函数,此处以 number_label 控件为例,该函数的基本框架通常如下:

/* awtk-widget-number-label/src/number_label/number_label.c */
ret_t number_label_on_event(widget_t* widget, event_t* e) {
  ret_t ret = RET_OK;
  number_label_t* number_label = NUMBER_LABEL(widget);
  /* 前置处理 */
  ......
  /* 根据不同的事件类型添加处理代码 */
  switch (e->type) {
    case /* 事件类型 */: {
      /* 事件处理代码 */
      break;
      }
    case EVT_KEY_DOWN: {
      key_event_t* evt = (key_event_t*)e;
      if (!(number_label->readonly)) {
       ......
      }
      break;
    }
    ......
  }

  return ret;
}

常用的控件事件类型详见下表,更多的请参考 events.h (opens new window)

事件类型 说明
EVT_POINTER_DOWN 指针按下事件
EVT_POINTER_MOVE 指针移动事件
EVT_POINTER_UP 指针抬起事件
EVT_KEY_DOWN 键按下事件
EVT_KEY_UP 键抬起事件
EVT_FOCUS 得到焦点事件
EVT_BLUR 失去焦点事件
等等......

另外,我们需要注意 on_event 函数的返回值:

  • 返回 RET_OK 表示事件处理完毕后继续向上传递,即控件的父控件会收到该事件。
  • 返回 RET_STOP 表示事件处理完毕之后停止传递。

# 3.2.7 设置/获取控件的属性

控件虚表结构体(widget_vtable_t)中 set_prop 函数和 get_prop 函数分别用来设置获取控件的属性,在调用 widget_set_prop() 函数和 widget_get_prop() 函数时,会回调到重载的控件函数中,此处以 number_label 控件为例,这两个重载函数的实现代码如下:

在控件的 set_prop 函数和 get_prop 函数中,属性的值采用 value_t 类型保存,该类型的定义与用法请查阅:value.h (opens new window)

/* awtk-widget-number-label/src/number_label/number_label.c */
static ret_t number_label_set_prop(widget_t* widget, const char* name, const value_t* v) {
  number_label_t* number_label = NUMBER_LABEL(widget);
  return_value_if_fail(widget != NULL && name != NULL && v != NULL, RET_BAD_PARAMS);

  if (tk_str_eq(name, WIDGET_PROP_MIN)) {
    number_label->min = value_double(v);
    return RET_OK;
  } else if (tk_str_eq(name, WIDGET_PROP_MAX)) {
    number_label->max = value_double(v);
    return RET_OK;
  }
  ......
  return RET_NOT_FOUND;
}
/* awtk-widget-number-label/src/number_label/number_label.c */
static ret_t number_label_get_prop(widget_t* widget, const char* name, value_t* v) {
  number_label_t* number_label = NUMBER_LABEL(widget);
  return_value_if_fail(widget != NULL && name != NULL && v != NULL, RET_BAD_PARAMS);

  if (tk_str_eq(name, WIDGET_PROP_MIN)) {
    value_set_double(v, number_label->min);
    return RET_OK;
  } else if (tk_str_eq(name, WIDGET_PROP_MAX)) {
    value_set_double(v, number_label->max);
    return RET_OK;
  } 
  ......
  return RET_NOT_FOUND;
}

# 3.2.8 控件的销毁回调函数

控件虚表结构体(widget_vtable_t)中 on_destroy 函数会在控件被销毁时回调执行,用于释放控件内部申请的内存或进行其他析构操作。

此处以 number_label 为例,该控件上保存了一个 malloc 出来 format 属性,在控件销毁时必须释放这块内存,代码如下:

/* awtk-widget-number-label/src/number_label/number_label.c */
static ret_t number_label_on_destroy(widget_t* widget) {
  number_label_t* number_label = NUMBER_LABEL(widget);
  return_value_if_fail(widget != NULL && number_label != NULL, RET_BAD_PARAMS);

  TKMEM_FREE(number_label->format);

  return RET_OK;
}

# 3.3 设置控件的初始属性和缺省样式

默认情况下,从 Designer 的控件列表的"自定义"分组中拖出一个控件,其初始属性将采用控件构造函数中值,并且没有缺省样式,这会导致控件的绘制函数无法获取对应的样式属性,比如文本颜色、文本大小、边距等等,画出来的控件就是全透明的。

如果需要指定控件的初始属性和缺省样式,可以在控件类型结构体(class)注释上补充如下格式的注释:

/**
...
 * ```xml
 * <!-- ui -->
 * 控件初始属性的 xml 描述(如果描述中包含子控件,会同时创建)
 * ```
...
 * ```xml
 * <!-- style -->
 * 控件默认样式的 xml 描述(如果描述中包含其它控件的样式,会同时添加到 default.xml 样式文件)
 * ```
...
 */

在 number_label 控件中,它的初始属性和缺省样式代码如下:

/* awtk-widget-number-label/src/number_label/number_label.h */
/**
 * @class number_label_t
 * @parent widget_t
 * @annotation ["scriptable","design","widget"]
 * 数值文本控件。
 *
 * 在 xml 中使用"number\_label"标签创建数值文本控件。如:
 *
 * ```xml
 * <!-- ui -->
 * <number_label x="c" y="50" w="24" h="100" value="40" format="%.4lf" decimal_font_size_scale="0.5"/>
 * ```
 *
 * 可用通过 style 来设置控件的显示风格,如字体的大小和颜色等等。如:
 * 
 * ```xml
 * <!-- style -->
 * <number_label>
 *   <style name="default" font_size="32">
 *     <normal text_color="black" />
 *   </style>
 *   <style name="green" font_name="led" font_size="32">
 *     <normal text_color="green" />
 *   </style>
 * </number_label>
 * ```
 */
typedef struct _number_label_t {
  ......
} number_label_t;

按照以上注释代码,在 Designer 中创建 number_label 控件时,其默认的 UI 代码如下:

<!-- UI 文件 -->
<number_label x="c" y="50" w="24" h="100" value="40" format="%.4lf" decimal_font_size_scale="0.5"/>

如果采用拖拽的方式创建控件,其 x、y 属性会被修改为鼠标抬起时的位置。

创建 number_label 控件后,注释中的缺省样式代码会被拷贝到项目的全局样式文件(default.xml),代码如下:

<!-- design/default/styles/default.xml -->
<number_label>
  <style name="default" font_size="32">
    <normal text_color="black" />
  </style>
  <style name="green" font_name="led" font_size="32">
    <normal text_color="green" />
  </style>
</number_label>

# 3.4 实现示例程序

开发完自定义控件之后,通常都需要在控件的示例程序中验证控件功能是否正常。使用 Designer 创建自定义控件项目时,会自动生成 demos 目录,该目录存放自定义控件的示例程序代码,用于展示控件效果并给用户提供简单的示例用法。

示例程序的实现方式与普通的 AWTK 应用程序一样,可以在界面上通过拖拽创建自定义控件,设置控件的属性和样式,编辑 demos 目录下的程序代码,打包资源并编译运行,如下图所示。

图3.2 实现示例程序
图3.2 实现示例程序

# 3.5 完善单元测试

使用 Designer 创建自定义控件项目时,会自动生成 tests 目录,该目录存放自定义控件的单元测试代码,用于测试控件逻辑代码的准确性与稳定性,它们默认基于 GTest(Google Test)框架 (opens new window) 实现,具体的使用方法可参考 GTest 的 官方文档 (opens new window)。number_label 控件的单元测试代码可参阅:tests/number_label_test.cc,如无需测试可跳过本小节,此处不过多赘述。

# 3.6 自定义控件的相关图标

如果需要修改自定义控件在 Designer 中的图标,请将图标存放到指定位置,详见下文,这些图标如果不指定,则统一显示缺省图标。

# 3.6.1 插件图标

插件图标指 Designer 的"插件管理"页面上用于标识自定义控件或者描述其功能的图标,大小为 60*60 像素,默认为自定义控件项目的 docs/images/widget_preview.png 文件。

number_label 控件的效果如下图所示:

图3.3 插件图标
图3.3 插件图标

# 3.6.2 控件列表上的图标

控件列表上的图标指 Designer 的控件列表上该控件显示的图标,大小为 48*48 像素,默认为自定义控件项目的 docs/images/widget_list.png 文件。

number_label 控件的效果如下图所示:

图3.4 控件列表上的图标
图3.4 控件列表上的图标

如果自定义控件库中包含多个控件,可以用 "widget_list_" + 控件类型名的形式,为控件单独指定图标,比如"widget_list_number_label.png"。

# 3.6.3 对象浏览器上的图标

对象浏览器上的图标指 Designer 的对象浏览器上该控件对象左侧显示的图标,大小为 16*16 像素,默认为自定义控件项目的 docs/images/widget_obj.png 文件。

number_label 控件的效果如下图所示:

图3.5 对象浏览器上的图标
图3.5 对象浏览器上的图标

如果自定义控件库包含多个控件,可以用 "widget_obj_" + 控件类型名的形式,为控件单独指定图标,比如"widget_obj_number_label.png"。

# 3.7 注册自定义控件

在 AWTK 项目中,使用自定义控件之前,需要先注册自定义控件,如果是在 Designer 中导入并安装自定义控件,那么 Designer 会自动添加这些注册代码。这里我们仅简单介绍一下注册的方法。

例如,我们在一个新建的 AWTK 项目中安装 number_label 控件,自定义控件代码会被放在项目的 3rd 目录下,注册控件的步骤如下:

步骤一:在程序初始化时,将自定义控件类型注册到 AWTK 的控件工厂,代码如下:

/* app/src/application.c */
#include "../3rd/awtk-widget-number-label/src/number_label_register.h"

/**
 * 注册自定义控件
 */
static ret_t custom_widgets_register(void) {
  number_label_register();  /* 注册 number_label 控件 */
  return RET_OK;
}
......
/**
 * 初始化程序
 */
ret_t application_init(void) {
  custom_widgets_register();
  ......
  return navigator_to(APP_START_PAGE);
}
/* app/3rd/awtk-widget-number-label/src/number_label_register.c */
ret_t number_label_register(void) {
  /* 将 number_label 控件类型注册到 AWTK 的控件工厂(将控件类型与对应的构造函数绑定) */
  return widget_factory_register(widget_factory(), WIDGET_TYPE_NUMBER_LABEL, number_label_create);
}

步骤二:在项目的编译脚本中添加自定义控件库,代码如下:

# app/SConstruct
......
# 设置自定义控件库的路径和名称
CUSTOM_WIDGET_LIBS = [{
    "root" : '3rd/awtk-widget-number-label', # 库路径
    'shared_libs': ['number_label'],         # 动态库名称
    'static_libs': []
}]

DEPENDS_LIBS = CUSTOM_WIDGET_LIBS + []

# 添加自定义控件库
helper = app.Helper(ARGUMENTS)
helper.set_deps(DEPENDS_LIBS)

app.prepare_depends_libs(ARGUMENTS, helper, DEPENDS_LIBS)
helper.call(DefaultEnvironment)
......