# 3. 开发基础

本章导读:

学习一门新的开发语言,首先得学习一些关于该门语言的基础知识,如:语法。同样的在学习AWTK的过程中,也需要学习一些基本的基础知识,在了解了这些知识之后,才能更加好的学习后面的章节。

# 3.1 简介

本章将按照AWTK框架的构成,阐述AWTK开发过程中所需的基础知识,包括窗体样式、布局管理器、定时器、事件处理等。本章以AWTK源码目录下自带的例子(awtk/demos)来介绍关于AWTK的基础知识,所以本章涉及的代码均可以在AWTK源码目录下找到。

# 3.2 窗体样式

设计漂亮的界面并非程序员的强项,AWTK通过窗体提供这样一种机制,让设计漂亮的界面变得非常容易。通过窗体样式,可以改变控件的背景颜色、边框颜色、字体颜色、字体、字体大小、背景图片、背景图片的显示方式和图标等属性。同时AWTK也提供了一些窗体样式重用的机制,让窗体样式文件的开发和维护变得容易。

# 3.2.1 窗体样式结构

AWTK的窗体样式按控件进行分类,每种控件可以有多种不同的样式,每种样式下又有不同状态下的配置,代码如下:

<!--awtk/demos/assets/default/raw/styles/default.xml -->
...
<label>
  <style name="default">
    <normal text_color="black" />
  </style>
  <style name="left">
    <normal text_color="red" text_align_h="left" border_color="#a0a0a0" margin="4" />
  </style>
...
</label>

上面是文本框样式配置(可以按需增加style的数量),其中定义两种不同的文本样式:

  • default 为缺省的文本样式(如果控件中不指定样式将使用default样式)
  • left为文本水平居左样式

窗体样式的各个属性,具体请看3.2.2章节,可以分别在控件、style或者状态中定义,优先级从底到高,重复定义时,后者会覆盖前者。具体说明如下:

(1)在控件中指定样式的属性,代码如下:

<label text_color="red">
  <style name="default">
    <normal border_color="black" />
  </style>
</label>

(2)在style中指定样式的属性,代码如下:

<label >
  <style name="default" text_color="red">
    <normal border_color="black" />
  </style>
</label>

(3)在style中指定样式的属性,代码如下:

<label >
  <style name="default" >
    <normal text_color="red" border_color="black" />
  </style>
</label>

在自己的应用程序assets/default/raw/styles目录下必须有default.xml文件(缺省窗体样式),否则控件将不能正常显示,并且最好在default.xml文件中给各个控件指定一个default缺省样式。

# 3.2.2 窗体样式属性

AWTK为控件提供了丰富的属性,详见下表,其中颜色可使用标准名称、#开头的16进制值和rgba合成的值,例如,在3.2.1章节中给label文本控件设置文本颜色为黑色。

在下表中的属性一般每个控件都可以使用,例如button按钮、label控件都可以设置bg_color属性。除非特别说明,例如selected_bg_color属性只能在edit编辑器中使用,具体每个控件可以设置哪些属性可以参考第4章下各个子章节的"样式"章节。

属性 说明
bg_color 背景颜色
fg_color 前景颜色
mask_color 蒙版颜色
font_name 字体名称
font_size 字体大小
text_color 文本颜色
tips_text_color 提示文本颜色
text_align_h 文本水平对齐的方式(取值:center|left|right)
text_align_v 文本垂直对齐的方式(取值:middle|top|bottom)
border_color 边框颜色
border_width 边框线宽
border 边框类型(取值:all|top|bottom|left|right)
bg_image 背景图片的名称
bg_image_draw_type 背景图片的显示方式
icon 图标的名称
fg_image 前景图片的名称
fg_image_draw_type 前景图片的显示方式
spacer 间距
margin 边距
margin_left 左边距
margin_right 右边距
margin_top 顶边距
margin_bottom 底边距
icon_at 图标的位置(取值:top|bottom|left|right)
active_icon active图标的名称
x_offset x方向的偏移,方便实现按下的效果
y_offset y方向的偏移,方便实现按下的效果
selected_bg_color 编辑器(包括edit和mledit控件)中选中区域的背景颜色
selected_fg_color 编辑器(包括edit和mledit控件)中选中区域的前景颜色
selected_text_color 编辑器(包括edit和mledit控件)中选中区域的文本颜色
round_radius 圆角半径(仅在with_vgcanvas定义时生效)
round_radius_top_left 左上角圆角半径(仅with_vgcanvas定义时生效)
round_radius_top_right 右上角圆角半径(仅with_vgcanvas定义时生效)
round_radius_bottom_left 左下角圆角半径(仅with_vgcanvas定义时生效)
round_radius_bottom_right 右下角圆角半径(仅with_vgcanvas定义时生效)

# 3.2.3 控件状态

在3.2.1章节中,为label指定了normal(正常状态)。AWTK为各种控件提供了不同状态的属性,详见下表,可以根据不同的状态设置不一样的样式。具体每个控件可以设置哪些状态可以参考第4章下各个子章节的"状态"章节,示例可以参考:awtk/demos/assets/default/raw/styles/default.xml。

状态 说明
normal 正常状态
pressed 指针按下状态
over 指针悬浮状态
disable 禁用状态
focused 聚焦状态
checked 勾选状态
unchecked 没勾选状态
empty 编辑器无内容状态
empty_focus 编辑器无内容同时聚焦的状态
error 输入错误状态
selected 选中状态
normal_of_checked 正常状态(选中项)
pressed_of_checked 指针按下状态(选中项)
over_of_checked 指针悬浮状态(选中项)
focused_of_checked 焦点状态(选中项)
normal_of_active 正常状态(当前项)
pressed_of_active 指针按下状态(当前项)
over_of_active 指针悬浮状态(当前项)
focused_of_active 焦点状态(当前项)

# 3.2.4 使用窗体样式

在窗体样式文件(通常是:default.xml)中定义好了控件的样式后,就可以在代码中使用了。例如,在3.2.1章节中定义了label控件的left样式,可以使用下面两种方式设置label控件的样式为left:

(1)UI文件方式,在XML文件中通过style属性设置label控件的样式为left,代码如下:

<!-- awtk/demos/assets/default/raw/ui/basic.xml -->
<window anim_hint="htranslate">
  ...
  <row x="0" y="80" w="100%" h="30" children_layout="default(r=1,c=3,xm=2,s=10)">
    <label style="left" name="left" text="Left"/>
    <label style="center" name="center" text="Center"/>
    <label style="right" name="right" text="Right"/>
  </row>
  ...
</window>

(2)C代码方式,在C源文件中可以调用widget_use_style()函数设置label控件的样式为left,代码如下:

/* awtk/demos/demo1_app.c */
ret_t application_init() {
  ...
  label = label_create(win, 10, 40, 80, 30);
  widget_set_text(label, L"Left");
  widget_use_style(label, "left");
  ...
}

# 3.2.5 每个窗口支持独立的窗体样式

在AWTK中为每个窗口UI文件可以有自己的窗体样式文件,可以通过下面方式指定窗口自己的窗体样式文件:

  • 通过窗口的theme属性来指定窗体样式文件名(方便多个窗口共用一个窗体样式文件),代码如下:
<!-- default/ui/home_page.xml -->
<window theme="home_page">
  <label name="label" x="187" y="151" w="160" h="28" text="Label"/>
</window>
  • 如果没有指定theme属性,以窗口的name属性作为窗口的窗体样式文件名,代码如下:
<!-- default/ui/home_page.xml -->
<window name="home_page">
  <label name="label" x="187" y="151" w="160" h="28" text="Label"/>
</window>

建议窗口的窗体样式文件名与UI的文件名相同,例如:上面代码中窗口的窗体样式文件名和UI的文件名都是home_page。窗口的窗体样式文件与缺省窗体样式文件(default.xml)放在同一目录下。

AWTK在查找控件设置的样式时,优先在窗口自己的窗体样式文件中查找;如果没有找到,再去为缺省的窗体样式文件(default.xml)中查找;如果该样式在缺省的窗体样式文件也没有找到(可能会因为样式字符串的书写错误,而找不到),则用缺省窗体样式文件中该控件的default样式。

例如,在awtk/demos/assets/default/raw/ui/目录下"kb_*.xml"键盘文件中,通过设置"theme"属性,多个键盘文件共用同一个窗体样式文件"keyboard.xml",代码如下:

<!-- awtk/demos/assets/default/raw/ui/kb_default.xml -->
<keyboard theme="keyboard" x="0" y="bottom" w="100%" h="40%">
  <pages x="0" y="bottom" w="100%" h="-28" active="4">
    <view name="upper" x="0" y="0" w="100%" h="100%" children_layout="default(r=4,c=1,s=2,m=2)">
      <group_box children_layout="default(r=1,c=10,s=2,m=2)">
        <button repeat="300" name="Q" text="Q"/>
        <button repeat="300" name="W" text="W"/>
        ...
      </group_box>
	...
</keyboard>
<!-- awtk/demos/assets/default/raw/styles/keyboard.xml -->
...
<button>
  <style name="highlight" border_color="#a0a0a0"  text_color="black">
    <normal     bg_color="#a8b0b0" />
    <pressed    bg_color="#c0c0c0" />
    <over       bg_color="#e0e0e0" />
    <disable    bg_color="gray" text_color="#d0d0d0" />
  </style>
</button>
...

# 3.2.6 inline style动态修改控件样式

窗体样式数据是只读的,它的好处是速度快,占用内存少。但在一些特殊情况下,希望通过函数直接修改控件的style,或者在UI描述的XML文件中直接设置控件的style,把这类style称为inline style,具体用法如下:

  • 在XML UI描述文件中使用inline style。

控件的属性名以"style:"开头表示这是一个inline属性:

style:状态:名称

下面表示设置正常状态的字体大小为16:

style:normal:font_size="16"

状态可以省略,如果省略,表示正常状态(normal),下面这个和上面的功能一样:

style:font_size="16"

完整示例:

<label x="0" y="0" w="100%" h="100%" text="hello" style:font_size="24" style:text_color="green"/>
<button name="dec_value" text="Dec" focusable="true" style:focused:text_color="red"/>
  • 在C代码中使用inline style。

在AWTK中,便于用户使用inline style,提供了下表中的函数,具体请查看AWTK_API手册,这里不累赘了。

函数名称 说明
widget_set_style_int 设置整数类型的style
widget_set_style_str 设置字符串类型的style
widget_set_style_color 设置颜色类型的style

示例:

/* 设置button控件的字体大小为24 */
widget_set_style_int(button, "font_size", 24);

/* 设置button控件正常状态下的背景色 */
widget_set_style_color(button, "normal:bg_color", 0xff0000ff);

/* 设置button控件被按下时的背景色 */
widget_set_style_str(button, "pressed:bg_color", "red");  

inline style会消耗更多内存,而且不方便切换窗体样式,一般应该尽量避免使用。

# 3.2.7 注意事项

建议直接将awtk/demos/assets/default/raw/styles/default.xml文件复制到自己的应用程序目录中,如:res/assets/default/raw/styles目录下。

如果需要添加新的样式直接在该文件中添加,需要注意不要删除原有的样式,如果删除可能会造成某些控件无法正常显示。例如,不能删除combobox_down样式,否则combo_box控件不能显示下拉按钮,代码如下:

<!-- awtk/demos/assets/default/raw/styles/default.xml -->
...
<button>
  <style name="default" border_color="#a0a0a0"  text_color="black">
    <normal     bg_color="#f0f0f0" />
    <pressed    bg_color="#c0c0c0" x_offset="1" y_offset="1"/>
    <over       bg_color="#e0e0e0" />
    <focused    bg_color="#e0e0e0" />
    <disable    bg_color="gray" text_color="#d0d0d0" />
  </style>
  
  <style name="combobox_down" border_color="#a0a0a0">
    <normal     bg_color="#f0f0f0" icon="arrow_down_n"/>
    <pressed    bg_color="#c0c0c0" icon="arrow_down_p"/>
    <over       bg_color="#e0e0e0" icon="arrow_down_o"/>
  </style>
...

# 3.3 布局管理器

# 3.3.1 为什么需要布局参数

如果界面上元素是预先知道的,而且窗口的大小也是固定的,通过可视化的工具,以所见即所得的方式,去创建界面是最轻松的方式。但是在下列情况下,使用布局参数却是更好的选择:

(1)窗口的大小是可以动态调整的。

(2)需要适应不同大小的屏幕。

(3)界面上的元素是动态的,需要用程序创建界面。

# 3.3.2 概述

AWTK的布局器(layouter)分为两类,一类用于对控件自身进行布局,另一类用于对子控件进行布局详见下图。

  • self_layout 对控件自身进行布局
  • children_layout 用于对子控件进行布局
图3.1 layouter布局器
图3.1 layouter布局器

AWTK提供了灵活的扩展机制,可以方便的扩展新的布局方式,所以self_layouter和children_layouter都是接口。

# 3.3.3 控件自身的布局

AWTK目前仅仅实现了缺省布局方式,以后陆续实现css、flex等布局方式,详见下图。

图3.2 self_layouter布局
图3.2 self_layouter布局

控件自身布局有下面两种写法:

方式一:使用self_layout属性指定控件的x、y、w、h,示例如下:

<button text="ok" self_layout="default(x=10,y=10,w=100,h=100)"/>

方式二:使用缺省布局参数指定控件的x、y、w、h,示例如下:

<button text="ok" x="10" y="10" w="100" h="100" />

由于这两种写法的效果都是一样的,在下面的示例中以第二种写法为例。

1. 参数

缺省布局中有5个参数,详见下表:

参数 说明
x x坐标
y y坐标
w 控件宽度
h 控件高度
floating 是否为浮动布局。如果设置为true,该控件不受父控件的children_layouter的影响

2. 使用方法

AWTK提供了多种控件自身布局方式,开发者可以根据自己的需求选择其中一种方式,具体的方式请看下文。

(1)像素方式

直接指定控件的x/y/w/h的像素值,这是缺省的方式,也是最不灵活的方式。示例如下:

  • 在XML界面描述文件中:
<button x="10" y="5" w="80" h="30" text="ok"/>
  • 在C代码中:
widget_move_resize(btn, 10, 5, 80, 30);

(2)百分比

x/w的值如果包含"%",则自动换算成相对其父控件宽度的百分比。y/h的值如果包含"%",则自动换算成相对其父控件高度的百分比。示例如下:

  • 在XML界面描述文件中:
<button x="10%" y="10" w="50%" h="30" text="ok"/>
  • 在C代码中:
widget_set_self_layout(btn, "default(x=10%,y=10,w=50%,h=30)");
widget_layout(btn);

(3)水平居中

让控件在水平方向上居中,只需要将x的值设置成"c"或者"center"即可。示例如下:

<button x="center" y="10" w="50%" h="30" text="ok"/>

(4)垂直居中

让控件在垂直方向上居中,只需要将y的值设置成"m"或者"middle"即可。示例如下:

<button x="center" y="middle" w="50%" h="30" text="ok"/>

(5)位于右边

让控件位于父控件的右侧,只需要将x的值设置成"r"或者"right"即可。示例如下:

<button x="right" y="10" w="50%" h="30" text="ok"/>

如果还想离右侧有一定距离,可以在right后指定距离的像素。示例如下:

<button x="right:20" y="10" w="50%" h="30" text="ok"/>

(6)位于底部

让控件位于父控件的底部,只需要将y的值设置成"b"或者"bottom"即可。示例如下:

<button x="10" y="bottom" w="50%" h="30" text="ok"/>

如果还想离底部有一定距离,可以在bottom后指定距离的像素。示例如下:

<button x="10" y="bottom:20" w="50%" h="30" text="ok"/>

(7)宽度和高度为负数

无论是像素模式还是百分比模式,宽度和高度均可为负数。

  • 宽度为负数。其值为父控件的宽度+该负值
  • 高度为负数。其值为父控件的高度+该负值

(8)浮动布局

如果floating设置为true,该控件不受父控件的children_layouter的影响。示例如下:

<button x="10" y="20" w="50" h="30" floating="true" "text="ok"/>

# 3.3.4 子控件的布局

AWTK目前仅仅实现了缺省布局方式,详见下图,以后将会陆续实现css flex等布局方式。

图3.3 子控件的布局
图3.3 子控件的布局

1. 语法

子控件布局器统一使用children_layout属性指定,其语法为:

缺省子控件布局器 => default '(' PARAM_LIST ')'
PARAM_LIST => PARAM | PARAM ',' PARAM_LIST

示例:

<view x="0" y="0" w="100%" h="100%" children_layout="default(c=2,r=8,m=5,s=5)">

2. 参数

为了方便父控件布局子控件,AWTK提供了下面几个参数:

参数 简写 说明
rows r 行数
cols c 列数
width w 子控件的宽度,可以用来计算列数,与cols互斥
height h 子控件的高度,可以用来计算行数,与rows互斥
x_margin xm 水平方向的边距
y_margin ym 垂直方向的边距
spacing s 子控件之间的间距
keep_invisible ki 是否给不可见的控件留位置(缺省否)
keep_disable kd 是否给不用的控件留位置(缺省是)

在C代码中,可以通过下面的函数设置这几个参数:

/**
 * @method widget_set_children_layout
 * 设置子控件的布局参数。
 * @annotation ["scriptable"]
 * @param {widget_t*} widget 控件对象。
 * @param {const char*} params 布局参数。
 * @return {ret_t} 返回RET_OK表示成功,否则表示失败。
 */
ret_t widget_set_children_layout(widget_t* widget, const char* params);

示例:

widget_set_children_layout(w, "default(r=2,c=2)");

XML中,可以通过children_layout属性设置:

 <column x="20" y="160" w="50%" h="60" children_layout="default(r=2,c=1,ym=2,s=10)" >
    <check_button name="c1" text="Book"/>
    <check_button name="c2" text="Food"/>
  </column>

下面介绍通过调整rows/cols两个参数,来实现不同的布局方式。

3. 使用方法

(1)缺省

在没有设置子控件布局参数时,采用缺省的布局方式,父控件啥事也不做,完全由子控件自己的布局参数决定。

(2)hbox水平布局

当rows=1,cols=0时,所有子控件在水平方向排成一行,可以实现水平布局功能,子控件的参数详见下表:

属性 说明
x 从左到右排列,由布局参数计算而出
y 为y_margin
w 由子控件自己决定
h 为父控件的高度-2*y_margin

示例:

<window>
  <view x="c" y="m" w="300" h="30" children_layout="default(r=1,c=0,s=5)">
      <button text="1" w="20%"/>
      <button text="2" w="30%"/>
      <button text="3" w="30%"/>
      <button text="4" w="20%"/>
  </view>
</window>

例如,将文件保存为t.xml,可用preview_ui(在awtk/bin目录下)预览效果详见下图,命令如下:

bin/preview_ui.exe t.xml 480 320
图3.4 水平布局
图3.4 水平布局

(3)vbox垂直布局

当cols=1,rows=0时,所有子控件在垂直方向排成一列,可以实现垂直布局功能,子控件的参数详见下表:

属性 说明
x x_margin
y 从上到下排列,由布局参数计算而出
w 为父控件的宽度-2*x_margin
h 由子控件自己决定

示例:

<window>
  <view x="c" y="m" w="80" h="200" children_layout="default(r=0,c=1,s=5)">
      <button text="1" h="20%"/>
      <button text="2" h="30%"/>
      <button text="3" h="30%"/>
      <button text="4" h="20%"/>
  </view>
</window>

例如,将文件保存为t.xml,可用preview_ui(在awtk/bin目录下)预览效果详见下图,命令如下:

bin/preview_ui.exe t.xml 480 320
图3.5 垂直布局
图3.5 垂直布局

(4)listbox列表布局

当cols=1,rows=N时,所有子控件在垂直方向排成一列,可以实现列表布局功能,子控件的参数详见下表:

属性 说明
x x_margin
y 从上到下排列,由布局参数计算而出
w 为父控件的宽度-2*x_margin
h 为父控件的高度(减去边距和间距)分成成N等分

示例:

<window>
  <view x="c" y="m" w="200" h="200" children_layout="default(r=4,c=1,s=5)">
      <button text="1" />
      <button text="2" />
      <button text="3" />
      <button text="4" />
  </view>
</window>

例如,将文件保存为t.xml,可用preview_ui(在awtk/bin目录下)预览效果详见下图,命令如下:

bin/preview_ui.exe t.xml 480 320
图3.6 列表布局
图3.6 列表布局

(5)grid网格布局

当cols=N,rows=N时,所有子控件放在M x N的网格中,可以实现网格布局功能,子控件无需指定x/y/w/h等参数。

示例:

<window>
  <view x="c" y="m" w="200" h="200" children_layout="default(r=2,c=2,s=5)">
      <button text="1" />
      <button text="2" />
      <button text="3" />
      <button text="4" />
  </view>
</window>

例如,将文件保存为t.xml,可用preview_ui(在awtk/bin目录下)预览效果详见下图,命令如下:

bin/preview_ui.exe t.xml 480 320
图3.7 网格布局
图3.7 网格布局

(6) floating浮动布局

如果子控件的floating属性设置为true,其不受children_layout的限制。

示例:

<window>
  <view x="c" y="m" w="200" h="200" children_layout="default(r=2,c=2,s=5)">
      <label text="1" />
      <label text="2" />
      <label text="3" />
      <label text="4" />
      <button text="floating" floating="true" x="c" y="m" w="80" h="30"/>
  </view>	
</window>

例如,将文件保存为t.xml,可用preview_ui(在awtk/bin目录下)预览效果如详见下图,命令如下:

bin/preview_ui.exe t.xml 480 320
图3.8 浮动布局
图3.8 浮动布局

4. 高级用法

(1)子控件布局器和子控件自身的布局参数结合。

为了更大的灵活性,缺省子控件布局器可以和子控件自身的参数结合起来。

示例:

<window>
  <view x="c" y="m" w="200" h="200" children_layout="default(r=2,c=2,s=5)">
      <button text="1" x="0" y="0" w="50%" h="50%"/>
      <button text="2" x="r" y="m" w="60%" h="60%"/>
      <button text="3" x="c" y="m" w="70%" h="70%"/>
      <button text="4" x="c" y="m" w="80%" h="80%"/>
  </view>
</window>

例如,将文件保存为t.xml,可用preview_ui(在awtk/bin目录下)预览效果如详见下图,命令如下:

bin/preview_ui.exe t.xml 480 320
图3.9 高级用法一
图3.9 高级用法一

(2)子控件自身的布局参数x/y/w/h均为像素方式时,需要用self_layout参数指定。

示例:

<window>
  <view x="c" y="m" w="200" h="200" children_layout="default(r=2,c=2,s=5)">
      <button text="1" self_layout="default(x=0,y=0,w=50,h=50)" />
      <button text="2" x="r" y="m" w="60%" h="60%"/>
      <button text="3" x="c" y="m" w="70%" h="70%"/>
      <button text="4" x="c" y="m" w="80%" h="80%"/>
  </view>
</window>

例如,将文件保存为t.xml,可用preview_ui(在awtk/bin目录下)预览效果如详见下图,命令如下:

bin/preview_ui.exe t.xml 480 320
图3.10 高级用法二
图3.10 高级用法二

# 3.4 资源管理器

这里的资源管理器并非Windows下的文件浏览器,而是负责对各种资源,比如字体、窗体样式、图片、界面数据、字符串和其它数据的进行集中管理的组件。引入资源管理器的目的有以下几个:

  • 让上层不需要了解存储的方式。在没有文件系统时或者内存紧缺时,把资源转成常量数组直接编译到代码中。在有文件系统而且内存充足时,资源放在文件系统中。在有网络时,资源也可以存放在服务器上(暂未实现)。资源管理器为上层提供统一的接口,让上层而不用关心底层的存储方式。

  • 让上层不需要了解资源的具体格式。比如一个名为earth.png的图片,没有文件系统或内存紧缺,图片直接用位图数据格式存在ROM中;而有文件系统时,则用PNG格式存放在文件系统中。资源管理器让上层不需要关心图片的格式,访问时指定图片的名称即可(不用指定扩展名)。

  • 让上层不需要了解屏幕的密度。不同的屏幕密度下需要加载不同的图片,比如MacPro的Retina屏就需要用双倍解析度的图片,否则就出现界面模糊。AWTK以后会支持PC软件和手机软件的开发,所以资源管理器需要为此提供支持,让上层不需关心屏幕的密度。

  • 对资源进行内存缓存。不同类型的资源使用方式是不一样的,比如字体和窗体样式加载之后会一直使用,UI文件在生成界面之后就暂时不需要了,PNG文件解码之后就只需要保留解码的位图数据即可。资源管理器配合图片管理器等其它组件实现资源的自动缓存。

资源管理器和资源管理相关的组件详见下图,网络加载暂未实现。

图3.11 资源管理器
图3.11 资源管理器

# 3.4.1 资源的生成

AWTK中的资源需要进行格式转换才能使用:

  • 在没有文件系统时或者内存紧缺时,需要把资源转成常量数组直接编译到代码中。
  • XML格式的UI文件需要转换成二进制的格式。
  • XML格式的窗体样式文件需要转换成二进制的格式。
  • TTF字体可以根据需要转换成位图字体。
  • PNG图片可以根据需要转换成位图图片。

# 3.4.2 相关工具

在awtk/bin目录下提供了多种工具用于生成资源,详见下表:

工具 说明
fontgen 位图字体生成工具
imagegen 位图图片生成工具
resgen 二进制文件生成资源常量数组
themegen XML窗体样式转换成二进制的窗体样式
xml_to_ui XML的界面描述格式转换二进制的界面描述格式

上面工具的使用方法请看:awtk/tools目录对应文件夹下的README.md文档。

awtk/scripts/update_res.py脚本通过调用以上工具批量转换整合项目的资源。

# 3.4.3 初始化

将资源生成常量数组直接编译到代码中时,其初始化过程为:

(1)包含相应的资源数据文件,代码如下:

/* awtk/demos/assets_default.inc */
...
#include "assets/default/inc/strings/en_US.data"
#include "assets/default/inc/strings/zh_CN.data"
#include "assets/default/inc/styles/default.data"
#include "assets/default/inc/images/check.res"
#include "assets/default/inc/images/checked.res"
#include "assets/default/inc/fonts/default.res"
#include "assets/default/inc/ui/main.data"
...

(2)将资源添加到资源管理器分两种情况:

  • 没有文件系统时(即没有定义宏WITH_FS_RES),调用assets_manager_add()函数将资源添加到资源管理器中,代码如下:
/* awtk/demos/assets_default.inc */
...
assets_manager_add(rm, ui_kb_ascii);
assets_manager_add(rm, ui_vgcanvas);
assets_manager_add(rm, ui_rich_text1);
assets_manager_add(rm, ui_slide_menu);
assets_manager_add(rm, ui_radial_gradient);
assets_manager_add(rm, ui_color_picker_simple);
...
  • 有文件系统时(即有定义宏WITH_FS_RES时,不需要(1)包含相应的数据文件这一步),一般不需要特殊处理,不过可以用assets_manager_preload()函数预加载资源,代码如下:
/* awtk/demos/assets_default.inc */
...
assets_manager_preload(am, ASSET_TYPE_FONT, "default");
assets_manager_preload(am, ASSET_TYPE_STYLE, "default");
...

# 3.4.4 使用方法

  • 加载图片

调用image_manager_get_bitmap()函数获取位图数据,并指定图片的名称即可。例如,有一张earth.png的图片,并且需要加载这张图片,代码如下:

bitmap_t img;
image_manager_get_bitmap(image_manager(), "earth", &img);
  • 使用UI数据

调用window_open()函数打开UI文件,并指定UI文件的名称即可。例如,有个name.xml文件,并且需要打开这个UI文件,代码如下:

widget_t* win = window_open(name);
  • 使用字体

一般在窗体样式文件中指定font_name属性即可。如果字体文件不是default.ttf,才需要指定font_name属性。例如,有个ap.ttf的字体文件,并且label文件控件中的center_ap样式需要使用ap.ttf字体,代码如下:

<!-- awtk/demos/assets/default/raw/styles/default.xml -->
<label>
    <style name="center_ap">
        <normal text_color="green"  font_name="ap" font_size="12"/>
    </style>
</label>
  • 使用窗体样式

一般在UI界面描述文件中指定style属性即可。例如,为label文本框控件指定left样式,代码如下:

<!--awtk/demos/assets/default/raw/ui/basic.xml -->
<row x="0" y="80" w="100%" h="30" children_layout="default(r=1,c=3,xm=2,s=10)">
	<label style="left" name="left" text="Left"/>
</row>

# 3.4.5 资源的名称

1. 基本用法

资源名称一般就是资源的文件名,不带文件扩展名。比如:图片名为test.png,那资源名称就是test。如果因为某种原因,把test.png换成了test.jpg,对代码并无影响。

对于assets/default/raw/data目录下的资源,由于其扩展名不固定,所以需要带扩展名才能访问。比如:资源文件名为"app.json"的文件,不能用"app"访问,而是用"app.json"才能访问。

2. 高级用法

(1)简介

对于图片和UI资源名称,AWTK还支持一种高级用法。例如,有下面几种场景:

  • 需要支持不同的分辨率。而在不同分辨率里,要使用不同的背景图片。
  • 需要支持竖屏和横屏。而有的界面自动排版不能满足需求,需要为竖屏和横屏各写一个XML文件。
  • 需要支持不同的语言。而在不同的语言里,有的图片是语言相关的。

为了解决上面这些问题,AWTK提供了名称表达式:

  • 名称中可以带变量和表达式。变量用${xxx}表示,xxx将被替换成实际的值。
  • 可以指定多个名称,依次匹配,直到找到的为止。多个名称之间用逗号分隔。

(2)示例

示例1:支持竖屏和横屏下使用不同图片。

例如:在images/xx目录下有bg_portrait_1.jpg和bg_landscape_1.jpg两张图片,在竖屏时,加载bg_portrait_1.jpg;在横屏时,加载bg_landscape_1.jgp,代码如下:

<!-- awtk/demos/assets/default/raw/styles/default.xml -->
<style name="sky">
    <normal bg_image="bg_${device_orientation}_1"/>
</style>

在竖屏下,相当于:

<style name="sky">
    <normal bg_image="bg_portrait_1"/>
</style>

在横屏下,相当于:

<style name="sky">
    <normal bg_image="bg_landscape_1"/>
</style>

示例2: 支持不同的语言时使用不同图片。

例如:在images/xx目录下有flag_CN.png和flag_US.png两张图片,在当前语言为中文时,加载flag_CN.png;为英文时,加载flag_US.png,代码如下:

<image image="flag_${country},flag_none" x="c" y="m:-80" w="80" h="80"/>

在locale为zh_CN时,依次查找flag_CN和flag_none两张图片,相当于:

<image image="flag_CN,flag_none" x="c" y="m:-80" w="80" h="80"/>

在locale为en_US时,依次查找flag_US和flag_none两张图片,相当于:

<image image="flag_US,flag_none" x="c" y="m:-80" w="80" h="80"/>

# 3.5 事件处理

事件一般用来响应各种行为。例如当键盘按下或者抬起时,会产生键盘按下或抬起事件。当某个窗口打开或者关闭的时候,产生一个打开或者关闭事件,用户可以在打开或者关闭窗口的时候,注册一个回调函数完成某些功能。大多数事件是作为用户行为的响应而产生的,但也有例外,如timer定时器事件、idle定时器事件。

# 3.5.1 事件处理机制

在AWTK内部,AWTK通过函数tk_run启动的主事件循环不停的捕捉用户触发的事件和事件队列中的事件,然后将它们转换成对应的事件,如:EVT_KEY_DOWN键盘按下事件、EVT_POINTER_DOWN指针按下事件等,并最终将这些事件发送到目的控件对象来完成上述一系列事件的处理,大致流程请看下图。

图3.12 事件处理机制
图3.12 事件处理机制

例如,当点击一个button按钮会触发EVT_POINTER_DOWN指针按下事件,主事件循环捕获到该消息,最终会将该消息发送给button控件的button_on_event函数处理。

# 3.5.2 事件分发

事件分发的形式有多种,最常见的是通过widget_dispatch()函数分发一个事件,例如,AWTK中比较常见的按钮点击事件EVT_CLICK(详见3.5.4章节)就是通过widget_dispatch分发的。还有一些其他事件,则可以通过main_loop_queue_event()函数分发。

1. widget_dispatch

widget_dispatch()函数的参数列表如下:

参数 类型 说明
返回值 ret_t 返回RET_OK表示成功,否则表示失败
widget widget_t* 控件对象
e event_t* 事件

widget_dispatch用于分发一个事件,只能用于GUI线程,如果需要用到多线程的话,请用idle_queue或timer_queue函数(可参考3.8章节)。

例如,在HelloWorld.Xml-Demo的基础上修改代码,当用户点击"inc"按钮的时候,调用widget_dispatch分发一个自定义事件EVT_USER_EVT,然后调用widget_on(可参考3.5.3章节)捕获该事件,实现下图红色方框中的两个label文本控件的内容同步更新,代码如下:

图3.13 HelloWorld.Xml-Demo
图3.13 HelloWorld.Xml-Demo
/* HelloWorld.Xml-Demo/src/window_main.c */
#define EVT_USER_EVT (EVT_USER_START + 1)

static ret_t on_inc_click(void* ctx, event_t* e) {
  widget_t* win = WIDGET(ctx);
  widget_t* label = widget_lookup(win, "label_4_btn", TRUE);
  label_add(label, 1);

  event_t user = event_init(EVT_USER_EVT, NULL);
  widget_dispatch(window_manager(), &user);

  return RET_OK;
}

static ret_t on_user_evt(void* ctx, event_t* e) {
  (void)e;

  widget_t* win = WIDGET(ctx);
  widget_t* label_4_edit = widget_lookup(win, "label_4_edit", TRUE);
  widget_t* label_4_btn = widget_lookup(win, "label_4_btn", TRUE);
  widget_set_text(label_4_edit, widget_get_text(label_4_btn));

  return RET_OK;
}

static ret_t on_changing(void* ctx, event_t* evt) {
  widget_t* target = WIDGET(evt->target);
  widget_t* win = WIDGET(ctx);
  widget_t* label = widget_lookup(win, "label_4_edit", TRUE);
  widget_set_text(label, target->text.str);

  return RET_OK;
}

static ret_t init_widget(void* ctx, const void* iter) {
  (void)ctx;
  widget_t* widget = WIDGET(iter);
  widget_t* win = widget_get_window(widget);

  if (widget->name != NULL) {
    const char* name = widget->name;
    if (tk_str_eq(name, "edit")) {
      widget_on(widget, EVT_VALUE_CHANGING, on_changing, win);
    } else if (tk_str_eq(name, "dec_btn")) {
      widget_on(widget, EVT_CLICK, on_dec_click, win);
    } else if (tk_str_eq(name, "inc_btn")) {
      widget_on(widget, EVT_CLICK, on_inc_click, win);
    }
  }

  return RET_OK;
}

ret_t application_init() {
  widget_t* win = window_open("main");
  if (win) {
    init_children_widget(win);
    widget_on(window_manager(), EVT_USER_EVT, on_user_evt, win);
  }
  return RET_OK;
}

2. main_loop_queue_event

main_loop_queue_event()函数的参数列表如下:

参数 类型 说明
返回值 ret_t 返回RET_OK表示成功,否则表示失败
l main_loop_t* 主循环事件对象,改值一般填写main_loop()
e const event_queue_req_t* 事件请求对象

事件请求 event_queue_req_t 成员详见以下代码:

typedef union _event_queue_req_t {
  event_t event;
  key_event_t key_event;
  pointer_event_t pointer_event;
  add_idle_t add_idle;
  add_timer_t add_timer;
} event_queue_req_t;

main_loop_queue_event用于非GUI线程(可用于多线程)增加一个事件,本函数向主循环事件队列中发送一个增加事件请求。event_queue_req_t可以请求多种事件,如key_event_t键盘事件、pointer_event_t指针事件、idle定时器事件以及timer定时器事件,对于这几种事件AWTK已经封装好了对应的API,用户可以直接调用,具体请看下表,相关示例可以参考awtk/demos/demo_thread_app.c,涉及传参部分的内容请查看AWTK_API手册。

事件请求类型 对应的API
key_event_t main_loop_post_key_event
pointer_event_t main_loop_post_pointer_event
add_idle_t idle_queue
add_timer_t timer_queue

虽然main_loop_queue_event可用于多线程分发自定义事件,但在使用的时候,如果在非GUI线程中访问GUI控件对象,可能存在安全隐患,请用idle_queue或timer_queue函数代替(可参考3.8)。

main_loop_post_key_event()接口只能在关闭宏WITH_SDL的情况下使用,即该接口仅支持嵌入式平台。

3. 手动触发键盘或者鼠标事件

AWTK已经封装好了键盘或者鼠标事件的函数,有关函数的定义具体请看:awtk/src/main_loop/main_loop_simple.h

  • 触发键盘事件
main_loop_post_key_event(main_loop(), TRUE, TK_KEY_F1);

其中,在main_loop_post_key_event()函数中第一个参数main_loop()是固定的,第二个参数表示键盘是否按下,第三个参数表示按下的键盘值(具体可参考本文7.4章节)。

  • 触发鼠标事件
main_loop_post_pointer_event(main_loop(), TRUE, 10, 20);

其中,在main_loop_post_pointer_event()函数中第一个参数main_loop()是固定的,第二个参数表示鼠标是否按下,第三、四个参数分别表示x、y坐标。

# 3.5.3 事件处理

通常事件分发后将会调用相应的回调函数,例如,在本文3.5.2章节的第一小节中,点击"inc"按钮后,AWTK会分发EVT_CLICK(可参考本文3.5.4章节)事件,然后通过widget_on注册控件事件,最后调用注册的on_inc_click回调函数,实现每按下该按钮加1的功能。

1. 注册控件事件

(1) 函数原型

uint32_t widget_on(widget_t* widget, event_type_t type, event_func_t on_event, void* ctx);

(2) 参数说明

widget_on()函数的参数说明详见下表:

参数 类型 说明
返回值 uint32_t 用于widget_off
widget widget_t* 控件对象
type event_type_t 事件类型
on_event event_func_t 事件处理函数
ctx void* 事件处理函数上下文

2. 注销控件事件

由于窗口关闭时会销毁其中所有控件,所以一般不需要手工去注销。如果确实需要,可以使用widget_off。例如,使用widget_on给某个控件注册了事件后,在符合某些场景下不需要该控件继续响应该事件,就可以使用widget_off注销事件。

(1)函数原型

ret_t widget_off(widget_t* widget, uint32_t id);

(2)参数说明

widget_off()函数的参数说明详见下表:

参数 类型 说明
返回值 ret_t 返回RET_OK表示成功,否则表示失败
widget widget_t* 控件对象
id uint32_t widget_on返回的IDs

3. 注册事件回调函数返回值

通过widget_on()注册控件事件,在其回调函数中需要设置返回值,可以设置的返回值通常有:RET_OK、RET_REMOVE和RET_STOP(也可以是RET_FAIL等其他值,其效果和RET_OK一样,但不建议这么做),其具体说明如下:

(1)RET_OK

返回RET_OK,表示可重复执行。

例如,在下图的程序界面中,XML代码如下:

图3.14 事件返回值
图3.14 事件返回值
<window name="record" anim_hint="htranslate(easing=cubic_out)" style:normal:bg_color="#000000">
  ...
  <list_view name="list_view" x="4" y="76" w="-8" h="400" default_item_height="60" >
    <scroll_view name="scroll_view" x="0" y="0" w="100%" h="100%" >
      <list_item name="list_item_test" h="40" style="odd_clickable" >
        <button  w="100%" h="100%" name="button_test" tr_text="button_test"/>
      </list_item>
	  ...
  </list_view>
</window>

上图对应的C逻辑代码如下,给按钮"button_test"注册EVT_CLICK事件的回调函数on_button_test,其返回值为RET_OK,表示每次点击"button_test"按钮的时候,都会执行该函数。

static ret_t on_list_item_test(void* ctx, event_t* e) {
  (void)e;
  (void)ctx;
  return RET_OK;
}

static ret_t on_button_test(void* ctx, event_t* e) {
  (void)e;
  (void)ctx;
  return RET_OK;
}

static ret_t on_button_test_pointer_up(void* ctx, event_t* e) {
  (void)e;
  (void)ctx;
  return RET_STOP;
}

static ret_t init_widget(void* ctx, const void* iter) {
  ...
  if (widget->name != NULL) {
    const char* name = widget->name;
    if (tk_str_eq(name, "list_item_test")) {
      widget_on(widget, EVT_CLICK, on_list_item_test, win);
    } else if (tk_str_eq(name, "button_test")) {
      widget_on(widget, EVT_CLICK, on_button_test, win);
      widget_on(widget, EVT_POINTER_UP, on_button_test_pointer_up, win);
    }
  }

  return RET_OK;
}

(2)RET_REMOVE

返回RET_REMOVE,表示只执行一次。

例如,上述C代码中修改回调函数on_button_test的返回值为RET_REMOVE,on_button_test函数只会执行一次,以后就不会再执行了。

(3)RET_STOP

返回RET_STOP,表示停止后续操作。

例如,在上图中list_view中list_item_test和button_test控件都注册了EVT_CLICK事件。如果只想执行button_test控件注册的EVT_CLICK事件回调函数,而不执行list_item_test控件的。这个时候可以给button_test控件注册EVT_POINTER_UP事件,并设置其回调函数的返回值为RET_STOP,那么将不会执行list_item_test注册的EVT_CLICK事件的回调函数on_list_item_test。

# 3.5.4 事件类型

AWTK提供了丰富的事件类型,如鼠标按下、移动、抬起事件;键盘按下、抬起事件等,详见下表,一般在widget_on函数中使用。

事件名称 说明
EVT_POINTER_DOWN 指针按下事件名(pointer_event_t)
EVT_POINTER_DOWN_BEFORE_CHILDREN 指针按下事件名,在子控件处理之前触发(pointer_event_t)
EVT_POINTER_MOVE 指针移动事件名(pointer_event_t)
EVT_POINTER_MOVE_BEFORE_CHILDREN 指针移动事件名,在子控件处理之前触发(pointer_event_t)
EVT_POINTER_UP 指针抬起事件名(pointer_event_t)
EVT_POINTER_UP_BEFORE_CHILDREN 指针抬起事件名,在子控件处理之前触发(pointer_event_t)
EVT_WHEEL 滚轮事件名(pointer_event_t)
EVT_POINTER_DOWN_ABORT 取消前一个指针按下事件名(pointer_event_t)
EVT_CONTEXT_MENU 右键/长按弹出上下文菜单的事件名(pointer_event_t)
EVT_POINTER_ENTER 指针进入事件名(pointer_event_t)
EVT_POINTER_LEAVE 指针离开事件名(pointer_event_t)
EVT_LONG_PRESS 长按事件名(pointer_event_t)
EVT_CLICK 点击事件名(pointer_event_t)
EVT_FOCUS 得到焦点事件名(event_t)
EVT_BLUR 失去焦点事件名(event_t)
EVT_KEY_DOWN 键按下事件名(key_event_t)
EVT_KEY_DOWN_BEFORE_CHILDREN 键按下事件名,在子控件处理之前触发(key_event_t)
EVT_KEY_REPEAT 按键repeat事件名(key_event_t)
EVT_KEY_UP 键抬起事件名(key_event_t)
EVT_KEY_UP_BEFORE_CHILDREN 键抬起事件名,在子控件处理之前触发(key_event_t)
EVT_WILL_MOVE 即将移动Widget的事件名(event_t)
EVT_MOVE 移动Widget的事件名(event_t)
EVT_WILL_RESIZE 即将调整Widget大小的事件名(event_t)
EVT_RESIZE 调整Widget大小的事件名(event_t)
EVT_WILL_MOVE_RESIZE 即将调整Widget大小/位置的事件名(event_t)
EVT_MOVE_RESIZE 调整Widget大小/位置的事件名(event_t)
EVT_VALUE_WILL_CHANGE 控件的值即将改变的事件名(event_t)
EVT_VALUE_CHANGED 控件的值改变的事件名(event_t)
EVT_VALUE_CHANGING 控件的值持续改变(如编辑器正在编辑)的事件名(event_t)
EVT_PAINT 绘制的事件名(paint_event_t)
EVT_BEFORE_PAINT 即将绘制的事件名(paint_event_t)
EVT_AFTER_PAINT 绘制完成的事件名(paint_event_t)
EVT_PAINT_DONE 绘制完成(canvas状态已经恢复)的事件名(paint_event_t)
EVT_LOCALE_CHANGED locale改变的事件(event_t)
EVT_ANIM_START 控件动画开始事件(event_t)
EVT_ANIM_STOP 控件动画被主动停止的事件(event_t)
EVT_ANIM_PAUSE 控件动画被暂停的事件(event_t)
EVT_ANIM_ONCE 控件动画yoyo/repeat时,完成一次的事件(event_t)
EVT_ANIM_END 控件动画完成事件(event_t)
EVT_WINDOW_LOAD 窗口加载完成事件(event_t)
EVT_WINDOW_WILL_OPEN 窗口即将打开事件(event_t)如果有窗口动画,在窗口动画开始前触发如果没有窗口动画,在窗口被加载后的下一次循环中触发
EVT_WINDOW_OPEN 窗口打开事件(event_t)如果有窗口动画,在窗口动画完成时触发如果没有窗口动画,在窗口被加载后的下一次循环中触发
EVT_WINDOW_TO_BACKGROUND 窗口被切换到后台事件(event_t)打开新窗口时,当前窗口被切换到后台时,对当前窗口触发本事件
EVT_WINDOW_TO_FOREGROUND 窗口被切换到前台事件(event_t)关闭当前窗口时,前一个窗口被切换到前台时,对前一个窗口触发本事件
EVT_WINDOW_CLOSE 窗口关闭事件
EVT_REQUEST_CLOSE_WINDOW 请求关闭窗口的事件(event_t)
EVT_TOP_WINDOW_CHANGED 顶层窗口改变的事件(window_event_t)
EVT_IM_COMMIT 输入法提交输入的文本事件(im_commit_event_t)
EVT_IM_SHOW_CANDIDATES 输入法请求显示候选字事件(im_candidates_event_t)
EVT_IM_ACTION 软键盘Action点击事件(event_t)
EVT_IM_ACTION_INFO 请求更新软键盘上的Action按钮的信息(im_action_button_info_event_t)
EVT_DRAG_START 开始拖动(event_t)
EVT_DRAG 拖动(event_t)
EVT_DRAG_END 结束拖动(event_t)
EVT_SCREEN_SAVER 在指定的时间内(WITH_SCREEN_SAVER_TIME),没有用户输入事件,由窗口管理器触发
EVT_LOW_MEMORY 内存不足
EVT_OUT_OF_MEMORY 内存耗尽
EVT_ORIENTATION_WILL_CHANGED 屏幕即将旋转
EVT_ORIENTATION_CHANGED 屏幕旋转
EVT_WIDGET_CREATED 控件创建事件
EVT_REQ_START event queue其它请求编号起始值
EVT_USER_START 用户定义事件起始值

在AWTK中event_t是事件基类,常用的pointer_event_t和key_event_t等都是它的子类。例如,只有将event_t类型的对象转换为具体子类key_event_t类型才能获取键盘具体按下的是哪个键。AWTK中具体有哪些事件类型请看:awtk/src/base/events.h

1. 键盘事件

例如,进入下文第二小节示例图中右侧的界面时,按下"F2"键,界面将回到图中左侧的界面。要完成这样的功能,可以使用widget_on注册EVT_KEY_DOWN键盘按下事件,然后根据键盘按下的是哪个键,在做相应的处理,代码如下。其中,F2对应AWTK中的键盘映射关系请看本文7.4章节。

/* awtk/demos/demo_ui_app.c */
static ret_t on_key_back_or_back_to_home(void* ctx, event_t* e) {
  key_event_t* evt = (key_event_t*)e;
  if (evt->key == TK_KEY_F2) {
    window_manager_back(WIDGET(ctx));
  } else if (evt->key == TK_KEY_F3) {
    window_manager_back_to_home(WIDGET(ctx));
  }

  return RET_OK;
}

ret_t application_init() {
  widget_t* wm = window_manager();

  tk_ext_widgets_init();

  /* enable screen saver */
  window_manager_set_screen_saver_time(wm, 180 * 1000);
  widget_on(wm, EVT_SCREEN_SAVER, on_screen_saver, NULL);

  widget_on(wm, EVT_KEY_DOWN, on_key_back_or_back_to_home, wm);
  widget_on(wm, EVT_BEFORE_PAINT, wm_on_before_paint, wm);
  widget_on(wm, EVT_AFTER_PAINT, wm_on_after_paint, wm);
  widget_on(wm, EVT_LOW_MEMORY, wm_on_low_memory, wm);
  widget_on(wm, EVT_OUT_OF_MEMORY, wm_on_out_of_memory, wm);
  widget_on(wm, EVT_REQUEST_QUIT_APP, wm_on_request_quit, wm);

  return show_preload_res_window();
}

2. 鼠标事件

例如,点击awtk/bin/demoui.exe中的"Inc"按钮将增加进度条的值,详见下图:

图3.15 点击事件
图3.15 点击事件

使用widget_on函数注册EVT_CLICK点击事件对应的回调函数,代码如下:

/* awtk/demos/demo_ui_app.c */
static ret_t on_inc(void* ctx, event_t* e) {
  widget_t* win = WIDGET(ctx);
  (void)e;

  progress_bar_animate_delta(win, "bar1", 10);
  progress_bar_animate_delta(win, "bar2", 10);

  return RET_OK;
}

static ret_t install_one(void* ctx, const void* iter) {
  widget_t* widget = WIDGET(iter);
  widget_t* win = widget_get_window(widget);

  if (widget->name != NULL) {
    const char* name = widget->name;
    if (strstr(name, "open:") != NULL) {
      widget_on(widget, EVT_CLICK, on_open_window, (void*)(name + 5));
      widget_on(widget, EVT_LONG_PRESS, on_open_window, (void*)(name + 5));
    } else if (tk_str_eq(name, "inc_value")) {
      widget_t* win = widget_get_window(widget);
      widget_on(widget, EVT_CLICK, on_inc, win);
    }
  }

  return RET_OK;
}

3. 控件事件

控件事件一般是指控件响应某种行为而触发的事件,例如,edit单行编辑器控件,在用户正在输入文本的时候会触发EVT_VALUE_CHANGING(文本正在改变事件),在输入完成后则会触发EVT_VALUE_CHANGED(文本已改变事件)。

例如,在3.5.2章节的第一小节中,当用户在edit控件输入文本的时候会触发EVT_VALUE_CHANGING事件,通过widget_on给该事件注册回调函数on_changing,实现label文本控件和edit控件内容同步更新的功能。

控件可以触发哪些事件,可以参考第4章中各章节下的"事件"。例如,edit可以触发的事件可以参考4.6.3章节。

4. 控件动画事件

控件动画事件是指控件动画在不同的状态时触发的事件,例如,开始、暂停、停止、结束等,具体的可以参考3.5.4章节表格中以"EVT_ANIM_"开头的事件,另外有关控件动画方面的介绍请看5.3章节。

# 3.6 定时器

AWTK提供了很便捷的定时器,包括添加、删除定时器等。

# 3.6.1 函数

timer提供的函数详见下表,具体请查看AWTK_API手册,这里不累赘了。

函数名称 说明
timer_add 增加一个timer
timer_count 返回timer的个数
timer_modify 修改指定的timer的duration,修改之后定时器重新开始计时
timer_next_time 返回最近的timer到期时间
timer_queue 用于非GUI线程增加一个timer,本函数向主循环的事件队列中发送一个增加timer的请求
timer_remove 删除指定的timer
timer_reset 重置指定的timer,重置之后定时器重新开始计时
timer_set_on_destroy 设置一个回调函数,在timer被销毁时调用(方便脚本语言去释放回调函数)

# 3.6.2 定时器回调函数返回值

调用timer_add函数添加定时器的时候,在其回调函数中需要设置返回值,可以设置的返回值通常有:RET_REPEAT和RET_REMOVE,其具体说明如下。

(1)返回RET_REPEAT,表示可重复执行。例如,在3.6.3章节的代码中定时器的回调函数on_timer的返回值为RET_REPEAT,表示每隔500ms都会执行该函数。

(2)返回RET_REMOVE,表示只执行一次。如果修改3.6.3章节的代码,定时器的回调函数on_timer的返回值为RET_REMOVE,表示该定时器只执行一次,以后就不再执行了。

# 3.6.3 示例

下面代码是awtk/bin/demo1.exe下定时更新进度条的一个示例,效果如下图所示。调用timer_add函数每隔500ms更新进度条的数值,代码如下:

/* awtk/demos/common.inc */
static ret_t on_timer(const timer_info_t* timer) {
  widget_t* progress_bar = (widget_t*)timer->ctx;
  uint8_t value = (PROGRESS_BAR(progress_bar)->value + 5) % 100;
  progress_bar_set_value(progress_bar, value);
  return RET_REPEAT;
}

/* awtk/demos/demo1_app.c */
ret_t application_init() { 
  ...
  progress_bar = progress_bar_create(win, 10, 80, 168, 20);
  widget_set_value(progress_bar, 40);
  timer_add(on_timer, progress_bar, 500);
  ...
}
图3.16 定时器示例
图3.16 定时器示例

# 3.7 idle定时器

idle可以看作是duration为0的定时器,idle可以用来实现一些异步处理。

# 3.7.1 函数

idle提供的函数详见下表:

函数名称 说明
idle_add 增加一个idle
idle_count 返回idle的个数
idle_queue 用于非GUI线程增加一个idle,本函数向主循环事件队列中发送一个增加idle的请求
idle_remove 删除指定的idle
idle_set_on_destroy 设置一个回调函数,在idle被销毁时调用(方便脚本语言去释放回调函数)

# 3.7.2 idle定时器回调函数返回值

idle定时器中回调函数的返回值与timer定时器的一样,请看3.6.2章节。

# 3.7.3 示例

在以下代码中调用idle_add添加一个idle定时器,因为duration为0,不用等到某个时间点到了才执行。在AWTK每次消息循环的时候都会调用idle_add中的回调函数something_on_idle。

static ret_t something_on_idle(const idle_info_t* info) {
widget_t* widget = WIDGET(info->ctx);
  edit_t* edit = EDIT(widget);
  ...
  return RET_REPEAT;
}

ret_t application_init() { 
  ...
  idle_add(something_on_idle, edit);
  ...
}

# 3.8 在多线程环境中发送事件

GUI控件只能在GUI 线程进行操作,非GUI线程想操作GUI控件,必须用idle_queue或timer_queue进行串行化。

# 3.8.1 相关函数

(1)idle_queue函数

  • 函数功能:

向主循环的事件队列提交一个增加idle的请求,GUI线程的主循环在处理事件队列时,会把该idle函数放到idle管理器中,在分发idle时,该idle函数在 GUI 线程执行。

  • 函数原型:
ret_t idle_queue (idle_func_t on_idle, void* ctx);
  • 参数说明:
参数 类型 说明
返回值 ret_t 返回RET_OK表示成功,否则表示失败
on_idle idle_func_t idle回调函数
ctx void* idle回调函数的上下文

(2)timer_queue函数

  • 函数功能:

向主循环的事件队列提交一个增加timer的请求,GUI 线程的主循环在处理事件队列时,会把该timer函数放到timer管理器中,在分发timer时,该timer函数在 GUI 线程执行。

  • 函数原型:
ret_t timer_queue(timer_func_t on_timer, void* ctx, uint32_t duration);
  • 参数说明:
参数 类型 说明
返回值 ret_t 返回RET_OK表示成功,否则表示失败
on_timer timer_func_t timer回调函数
ctx void* timer回调函数的上下文
duration uint32_t 时间

需要注意的是,之所以 idle_queue 和 timer_queue 是少数几个可以在非GUI线程安全调用的函数,是因为这两个函数向主循环的事件队列添加idle或timer事件请求时是有加锁的,GUI线程的主循环在处理事件队列时,根据idle或timer请求类型(REQ_ADD_IDLE或REQ_ADD_TIMER),最终调用 idle_add 和 timer_add 函数,串行化执行。

# 3.8.2 示例

在以下代码中,调用tk_thread_create创建一个线程,在线程的test_timer_queue回调函数中调用timer_queue添加timer事件请求,在timer定时器中更新label文本框的内容。

static widget_t* s_label = NULL;

static ret_t update_label(int nr) {
  if (s_label) {
    char str[16] = {0};
    tk_snprintf(str, sizeof(str), "%d", nr);
    widget_set_text_utf8(s_label, str);
  }

   return nr > 0 ? RET_OK : RET_REMOVE;
}

/* GUI线程中可调用与GUI相关的函数,如:widget_lookup */
static ret_t on_timer(const timer_info_t* timer) {
   return update_label(*(int*)(timer->ctx));
}

/* 非GUI线程中不可调用与GUI相关的函数,如:widget_lookup */
void* test_timer_queue(void* args) {
  static int nr = 500000;
  while (nr-- > 0) {
    timer_queue(on_timer, &nr, 30);
    sleep_ms(30);
  }

  return NULL;
}

ret_t application_init() {
  tk_thread_t* thread = NULL;

  widget_t* win = window_create(NULL, 0, 0, 0, 0);
  widget_t* label = label_create(win, 10, 10, 300, 20);
  s_label = label;

  thread = tk_thread_create(test_timer_queue, NULL);
  tk_thread_start(thread);

  return RET_OK;
}

ret_t application_exit() {
  log_debug("application_exit\n");
  return RET_OK;
}

#include "awtk_main.inc"

在上面的代码中,需要注意的是:如果在调用tk_thread_create函数创建线程的时候,使用label对象作为线程的参数,并在update_label函数中使用该对象更新label文本框的内容,会存在安全隐患。因为label是GUI界面中的一个文本框控件对象,在线程操作的过程中,如果此时label对象在其他地方被销毁,再更新其文本内容就会出现问题。

# 3.9 基本函数库

AWTK提供容器与基本数据结构、流、并发、事件与事件源、压缩与解压等API接口,相关API的用法请查看AWTK_API.chm手册(下载地址请看:1.5.1),这里不累赘了。另外,对相关API的用法也可参考AWTK源码下的tests目录。

# 3.9.1 容器与基本数据结构

AWTK提供了一些常用的操作数组、链表、字符串等API接口,详见下表:

类型 说明
darray 动态数组
slist 单向链表
wbuffer Write Buffer
rbuffer Read Buffer
ring_buffer 循环缓存区
str 可变长度的 UTF8 字符串
wstr 可变长度的宽字符字符串
point
rect 矩形
color 颜色对象
pointf 点(浮点数格式)
int_str 数字-字符串类型
str_str 字符串-字符串类型
named_value 命名的值
value 一个通用数据类型,用来存放整数、浮点数、字符串和其它对象
rgba 颜色值

# 3.9.2 流

AWTK提供了一些常用的文件、套接字、串口等输入输出流API接口,详见下表:

类型 说明
istream 输入流的接口
ostream 输出流的接口
iostream 输入/出流的接口
iostream_mem 内存输入输出流
istream_mem 内存输入流
ostream_mem 内存输出流
istream_file 文件输入流
ostream_file 文件输出流
iostream_tcp TCP输入输出流
istream_tcp TCP输入流
ostream_tcp TCP输出流
iostream_udp UDP输入输出流
istream_udp UDP输入流
ostream_udp UDP输出流
iostream_serial 串口输入输出流
istream_serial 串口输入流
ostream_serial 串口输出流
istream_buffered 缓冲输入流
ostream_buffered 缓冲输出流
iostream_noisy 故障注入流
ostream_noisy 故障注入输出流
iostream_shdlc SHDLC输入输出流
istream_shdlc SHDLC输入流
ostream_shdlc SHDLC输出流
ostream_retry 重传输出流

# 3.9.3 并发

AWTK提供了互斥锁、线程、信号量和条件变量的API接口,详见下表:

类型 说明
mutex 互斥锁
thread 线程
semaphor 信号量
cond_var 简化版条件变量

# 3.9.4 压缩和解压

AWTK提供了压缩和解压的API接口,详见下表:

类型 说明
compressor 压缩解压接口
compressor_miniz 基于miniz实现的压缩解压接口

# 3.9.5 平台相关

AWTK提供了操作文件、目录以及获取时间等API接口,详见下表:

类型 说明
fs 文件系统
fs_dir 目录
fs_file 文件
fs_item 目录或目录
path 路径相关的工具函数
memory 内存管理相关函数和宏
date_time 时间日期函数
time_now 获取当前时间的函数
platform 平台接口,包括:获取时间、休眠等函数
socket_pair 生成两个可以互相通信的socket句柄

# 3.9.6 数据格式

AWTK提供了json的相关API接口,详见下表:

类型 说明
ubjson_parser json解析
ubjson_writer json写
ubjson_reader json读

# 3.9.7 工具类

AWTK提供了一些工具类、chat和wchar互转等API接口,详见下表:

类型 说明
utils 工具类
utf8 wchar_t和char类型转换接口
tokenizer 从字符串中解析出一个一个的 token