# 7. 输入法

本章导读:

AWTK提供了多种输入键盘,可以根据应用的具体情况灵活搭配选择一种或者多种键盘,还可以根据自己的喜好设置不同的窗体样式。

# 7.1 简介

输入法是GUI重要的组件之一,虽然实现起来并不是太复杂,但其涉及的组件比较多,理解起来还是比较困难的,这里介绍一下AWTK中输入法的内部架构,详见下图:

图7.1 输入法内部架构图
图7.1 输入法内部架构图
  • input_method sdl实现包装了原生输入法(SDL的实现有些问题,还需要进一步完善)
  • input_method_null实现只是提供了空的实现,不启用输入法和软键盘
  • input_method default提供了AWTK自己的实现,负责软键盘的打开关闭和输入法引擎的创建

# 7.2 软键盘

在嵌入式系统中,通常没有物理键盘,所以需要在屏幕上实现软键盘。AWTK中的软键盘是一个普通的窗口,其中的按钮和候选字控件,都是用AWTK的UI描述文件定义的,可以方便实现各种不同的软键盘。软键盘的描述文件放在awtk/demos/assets/default/raw/ui目录下,文件名以kb_打头。与普通窗口相比,软键盘有以下不同:

  • 被点击时不会影响原有编辑器的焦点
  • 不接受按键事件和输入文本事件

在AWTK给的演示示例awtk-examples目录下的demo默认都启动了软键盘功能。例如,在UI界面的XML文件中添加edit编辑器控件,并设置好input_type输入类型,就可以弹出软键盘了,具体示例请看7.5章节。如果不想启动键盘请添加宏WITH_NULL_IM。

# 7.3 键盘类型

AWTK为了开发方便,提供了多种类型的键盘,详见下表。其中"输入类型"对应edit编辑控件的input_type属性,代码详见7.5章节。

输入类型 键盘描述文件 说明
phone kb_phone 电话号码
int kb_int 整数
float kb_float 浮点数
uint kb_uint 非负整数
ufloat kb_ufloat 非负浮点数
hex kb_hex 16进制
email kb_ascii 邮件地址
password kb_ascii 密码
custom 自定义的键盘的XML文件 使用自定义的键盘
custom_password 自定义的密码软键盘的XML文件 使用自定义的密码软键盘
ipv4 kb_uint IP V4 地址(如:192.168.1.1)
date kb_uint 日期(如:2020/02/20)
time kb_uint 时间(时分,如:12:00)
time_full kb_uint 时间(时分秒,如:12:00:00)
不输入 kb_default 默认

# 7.4 键盘映射

AWTK提供了一套自己的键盘映射关系,详见下表,因为内容过多该表没有列出全部,完整的映射关系请看:在AWTK_API手册根据关键字key_code_t查看。

例如,在键盘上按下F2键对应在AWTK的是TK_KEY_F2,相关示例请看3.5.4章节的第一小节。

名称 说明
TK_KEY_RETURN 回车键
TK_KEY_BACKSPACE 删除键
TK_KEY_TAB Tab键
TK_KEY_SPACE 空格键
TK_KEY_0 数字键0
TK_KEY_1 数字键1
TK_KEY_2 数字键2
TK_KEY_a 字母键a
TK_KEY_b 字母键b
TK_KEY_F1 F1键
TK_KEY_F2 F2键
... ...

# 7.5 示例

因为程序不一定需要键盘,如果程序需要使用键盘需要做下面两件事:

  • 拷贝UI文件:根据需要将awtk/demos/assets/default/raw/ui目录下文件名以kb_打头UI文件拷贝到应用程序assets/default/raw/ui目录下。
  • 拷贝图片:因为有些键盘还需要使用到图片(使用到的图片可以打开kb_开头的UI文件看看),所以还需要将awtk/demos/assets/default/raw/images下对应的图片拷贝到应用程序assets/default/raw/images目录下。

下面以awtk/bin/demoui.exe下的KeyBoard页面为例。当点击edit编辑框控件的时候,AWTK会根据edit设置的input_type键盘类型,弹出不同的键盘,以kb_开头的键盘UI文件还可以根据实际的屏幕大小适当的修改它的高度以及样式等。如果edit输入类型不填,将默认使用kb_default键盘,效果如下:

图7.2 键盘
图7.2 键盘

UI文件具体代码如下:

<!-- awtk/demos/assets/default/raw/ui/keyboard.xml -->
<window anim_hint="htranslate">
  <list_view x="0"  y="0" w="100%" h="-50" item_height="36" auto_hide_scroll_bar="true">
    <scroll_view name="view" x="0"  y="0" w="-12" h="100%">
     <list_item style="empty" children_layout="default(r=1,c=0)">
      <label w="30%" text="Name"/>
      <edit w="70%" text="" tips="name"/>
     </list_item>
     <list_item style="empty" children_layout="default(r=1,c=0)">
      <label w="30%" text="Desc"/>
      <edit w="70%" text="" tips="desc"/>
     </list_item>
     <list_item style="empty" children_layout="default(r=1,c=0)">
      <label w="30%" text="Int"/>
      <edit w="70%" text="" tips="int" input_type="int"/>
     </list_item>
     <list_item style="empty" children_layout="default(r=1,c=0)">
      <label w="30%" text="UInt"/>
      <edit w="70%" text="" tips="unsigned int" input_type="uint"/>
     </list_item>
     <list_item style="empty" children_layout="default(r=1,c=0)">
      <label w="30%" text="Float"/>
      <edit w="70%" text="" tips="float" input_type="float"/>
     </list_item>
     ...
</window>

# 7.6 自定义键盘

有时AWTK自带的键盘,可能不能满足应用程序的需要。例如,需要将软键盘嵌入到窗口内部(比如计算器和密码输入等),这时可以使用自定义软键盘,这里以AWTK中的demoui为例,详见下图:

图7.3 自定义键盘
图7.3 自定义键盘

实现自定义键盘的步骤如下:

(1) 设置edit的input_type为"custom"(它会禁止启用内置的软键盘),代码如下:

<!-- awtk/demos/assets/default/raw/ui/soft_keyboard.xml -->
<window text="Custom Soft Keyboard" anim_hint="htranslate" >
  <edit x="c" y="10" w="90%" h="30" focus="true" input_type="custom" text="" tips="custom"/>
  ...
</window>

如果希望初始化时编辑器自动获的焦点,可以设置focus为true。

(2) 软键盘的按钮放入一个view(任何容器控件均可)中,并将view的is_keyboard设置为true,代码如下:

<!-- awtk/demos/assets/default/raw/ui/soft_keyboard.xml -->
<window text="Custom Soft Keyboard" anim_hint="htranslate" >
  ... 
  <view y="60" x="c" w="90%" h="-60" is_keyboard="true" 
    children_layout="default(r=4,c=4,m=5,s=5)" >
    <button name="key" text="0" />
    <button name="key" text="1" />
    <button name="key" text="2" />
    <button name="key" text="3" />
    <button name="key" text="4" />
    <button name="key" text="5" />
    <button name="key" text="6" />
    <button name="key" text="7" />
    <button name="key" text="8" />
    <button name="key" text="9" />
    <button name="key" text="#" />
    <button name="backspace" text="<=" />
  </view>
</window>

(3) 处理按钮事件

处理正常按键,代码如下:

/* awtk/demos/demo_ui_app.c */
static ret_t on_send_key(void* ctx, event_t* e) {
  widget_t* button = WIDGET(e->target);
  char text[2];
  text[0] = (char)button->text.str[0];
  text[1] = '\0';

  input_method_commit_text(input_method(), text);

  return RET_OK;
} 

处理删除键,代码如下:

/* awtk/demos/demo_ui_app.c */

static ret_t on_backspace(void* ctx, event_t* e) {
  input_method_dispatch_key(input_method(), TK_KEY_BACKSPACE);

  return RET_OK;
} 

如果你不希望出现编辑器的光标,可以使用label控件代替edit控件,输入和删除时直接操作label的text。

# 7.7 软键盘的action按钮

在Android、iPhone等手机的软键盘上有一个特殊的按钮,这个按钮的功能和显示的文本与当前的编辑操作密切相关,在不同的编辑器(edit控件)中,可能显示"发送"、"下一个"、"回车"等。

AWTK的软键盘也有这样一个特殊的按钮,称为软键盘的action按钮。在AWTK内置的软键盘中,该action按钮的默认文本为"Return",例如kb_default软键盘。

下面以demoui为例,实现Text编辑器唤醒kb_default软键盘时,action按钮显示文本为"Next",如下图所示,实现步骤请看下文。

图7.4 Text编辑器唤醒kb_default软键盘
图7.4 Text编辑器唤醒kb_default软键盘

# 7.7.1 添加action按钮

在软键盘的UI文件(kb_default.xml)中定义action按钮,即将button的name属性设置为action,代码如下:

<!-- awtk/design/default/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=0,s=2,m=2)">
        ...
        <button name="action" style="highlight" w="20%" text="Return"/>
      </group_box>
    </view>
    ...
</pages>
  ...
</keyboard>

# 7.7.2 修改action按钮显示文本

将Text编辑器(edit控件)的action_text属性设置为Next,当该编辑器唤醒软键盘时会根据action_text属性设置软键盘中action按钮的文本,代码如下:

<!-- awtk/design/default/ui/edit.xml -->
<window anim_hint="htranslate" >
  <list_view x="0"  y="0" w="100%" h="-50" item_height="36" auto_hide_scroll_bar="true">
    <scroll_view name="view" x="0"  y="0" w="-12" h="100%">
     ...
      <list_item style="empty" children_layout="default(r=1,c=0,ym=1)">
      <label w="30%" text="Text"/>
      <edit name="edit" w="70%" left_margin="34" tips="searth" min="0" max="150" 
            step="0.1" action_text="Next">
        <image draw_type="icon" image="find" x="0" y="0" w="30" h="100%" />
      </edit>
     </list_item>
    ...
    </scroll_view>
    <scroll_bar_d name="bar" x="right" y="0" w="12" h="100%" value="0"/>
  </list_view>
  <button name="close" x="center" y="bottom:10" w="25%" h="30" text="Close"/>
</window>

需要注意的是:以上代码中设置了kb_default软键盘action按钮的text为Return,因此该软键盘的action按钮默认显示为"Return"。

如果没有设置编辑框的action_text属性,那么被唤醒的软键盘的action按钮的显示其text属性的值,此处为"Return";如果设置了编辑框的action_text属性,那么被唤醒的软键盘的action按钮优先显示编辑框action_text属性的值。

# 7.7.3 实现action按钮功能

实现action按钮的自定义功能,可以注册对应编辑器EVT_IM_ACTION事件,例如,在指定编辑器唤醒的软键盘中点击action按钮,打印该编辑器设置的action_text,代码如下:

/* EVT_IM_ACTION事件的回调函数 */
static ret_t on_action_event(void* ctx, event_t* evt) {
  widget_t* target = WIDGET(evt->target);
  edit_t* edit = EDIT(target);
  log_debug("edit action_text: %s \n", edit->action_text);  

  return RET_OK;
}

/* 注册编辑器EVT_IM_ACTION事件 */
ret_t application_init(void) {
  widget_t* win = window_open("home_page");
  widget_t* edit = widget_lookup(win, "edit", TRUE);
  widget_on(edit, EVT_IM_ACTION, on_action_event, NULL);

  return RET_OK;
}

完整示例请参考 edit.c (opens new window)

# 7.8 加入中文输入法

在有些示例项目中,没有加入输入法,主要是开发板的flash不够。如果flash够大(不小于4M时),可以按下面步骤加入中文输入法,步骤如下:

(1)添加AWTK目录下的相关源代码

  • 加入3rd/gpinyin/src中的代码
  • 加入src/input_engines/input_engine_pinyin.cpp
  • 去掉src/input_engines/input_engine_null.cpp
  • include路径加入3rd/gpinyin/include
  • 把3rd/gpinyin/data目录下的gpinyin.dat文件(拼音输入法字典)拷贝到项目的资源目录中,例如:res/assets/default/raw/data。
  • 把default_full.ttf拷贝到default.tff,重新生成资源并编译(defualt.ttf没有汉字,default_full.ttf里才有,否则输入的字符无法显示)。

(2)拼音输入法需要:设置INPUT_ENGINE='pinyin'以及定义宏WITH_IME_PINYIN,具体可修改awtk/awtk_config.py文件,代码如下:

# awtk/awtk_config.py
...
#INPUT_ENGINE='null'
#INPUT_ENGINE='spinyin'
#INPUT_ENGINE='t9'
#INPUT_ENGINE='t9ext'
INPUT_ENGINE='pinyin'
...
if INPUT_ENGINE == 't9':
    COMMON_CCFLAGS = COMMON_CCFLAGS + ' -DWITH_IME_T9 '
elif INPUT_ENGINE == 't9ext' :
    COMMON_CCFLAGS = COMMON_CCFLAGS + ' -DWITH_IME_T9EXT'
elif INPUT_ENGINE == 'pinyin' :
    COMMON_CCFLAGS = COMMON_CCFLAGS + ' -DWITH_IME_PINYIN '
elif INPUT_ENGINE == 'spinyin' :
    COMMON_CCFLAGS = COMMON_CCFLAGS + ' -DWITH_IME_SPINYIN '
elif INPUT_ENGINE == 'null' :
    COMMON_CCFLAGS = COMMON_CCFLAGS + ' -DWITH_IME_NULL '
  ...

关于输入法的其他说明,请看:awtk/src/input_engines/README.md文档。

(3)在UI界面XML中添加edit编辑器控件,不要设置input_type属性,点击下图中右侧edit编辑器控件可以弹出默认中文输入法键盘。

图7.5 弹出中文输入法
图7.5 弹出中文输入法

具体代码如下:

<!-- awtk/demos/assets/default/raw/ui/edit.xml -->
<window anim_hint="htranslate" >
  <list_view x="0"  y="0" w="100%" h="-50" item_height="36" auto_hide_scroll_bar="true">
    <scroll_view name="view" x="0"  y="0" w="-12" h="100%">
	...
     <list_item style="empty" children_layout="default(r=1,c=0,ym=1)">
      <label w="30%" text="Text"/>
      <edit w="70%" left_margin="34" tips="searth" min="0" max="150" step="0.1">
        <image draw_type="icon" image="find" x="0" y="0" w="30" h="100%" />
      </edit>
     </list_item>
     ...
    </scroll_view>
    <scroll_bar_d name="bar" x="right" y="0" w="12" h="100%" value="0"/>
  </list_view>
  <button name="close" x="center" y="bottom:10" w="25%" h="30" text="Close"/>
</window>

# 7.9 更新拼音输入法字典和联想字库

拼音输入法字典保存了输入的拼音和出现的汉字之间的对应关系,例如输入拼音"wo",会出现汉字"我",如下图左侧所示。

联想字库是指在输入了某个汉字或词组后,输入法根据该汉字或词组提供其常用的组词,例如输入汉字"我"后,根据常用组词"我们"、"我国"、"我省"等,输入法会提供"们"、"国"、"省"等汉字,如下图右侧所示。

图7.6 拼音输入法字典(左侧)和联想字库(右侧)
图7.6 拼音输入法字典(左侧)和联想字库(右侧)

在某些情况下,开发者需要自己更新拼音输入法字典和联想字库。比如:使用更好的输入法字典、去掉一些不需要的汉字或者使用更完善的联想字库,具体方法详见下文。

# 7.9.1 更新拼音输入法字典

更新拼音输入法字典的步骤如下:

1. 修改配置文件

(1)配置文件awtk/3rd/gpinyin/data/valid_utf16.txt:

该文件中存放了有效的utf16编码的汉字。汉字要正常显示,则必须要存在于该文件中,当需要去掉一些不必要的汉字时,可以删除该文件中对应的汉字,但该文件通常不做修改。

(2)配置文件awtk/3rd/gpinyin/data/rawdict_utf16_65105_freq.txt:

该文件中存放了拼音对应的汉字以及该汉字的使用频率,使用频率越高,输入拼音时对应汉字的排名就越前,比如配置文件中"戏剧"的使用频率比"喜剧"高,那么在输入拼音"xiju"时,"戏剧"就排在"喜剧"之前,如下图所示。

如果想要输入拼音"xiju"时,"喜剧"排在"戏剧"之前,只需让"喜剧"的使用频率高于"戏剧"即可,例如此处将"喜剧"的使用频率改为1300.00000000即可。

图7.7 rawdict_utf16_65105_freq.txt文件
图7.7 rawdict_utf16_65105_freq.txt文件

2. 重新生成字典数据

修改完配置文件后,需要重新生成字典数据,在awtk目录下打开终端,运行awtk/bin目录下的gpinyingen程序,执行以下命令:

./bin/gpinyingen

该程序通过上面两个配置文件生成awtk/3rd/gpinyin/data/gpinyin.dat,该文件就是更新后的拼音输入法字典。

3. 在项目中使用gpinyin.dat

将生成的awtk/3rd/gpinyin/data/gpinyin.dat文件拷贝到项目资源目录的data目录下,例如:res/assets/default/raw/data。

若项目是用Designer创建的,那么可以将gpinyin.dat文件拷贝到项目的design/default/data目录中,再使用Designer打包项目资源。

# 7.9.2 更新联想字库

更新拼音输入法的联想字库有两种方式,一种是抓取网页数据生成words.json文件,另一种是直接使用现有数据生成words.json文件,然后通过words.json生成words.bin文件,最终通过words.bin文件更新联想字库。

由于更新联想字库需要执行js脚本文件,所以开发者需要先安装nodejs,安装过程可自行上网搜索。更新联想字库的步骤如下:

(1)生成words.json文件

方式一:通过抓取网页数据生成words.json,在awtk/tools/word_gen目录中打开终端,执行脚本gen_words_json.js,抓取网页,生成words.json文件,命令如下:

node gen_words_json.js

可修改脚本 gen_words_json.js 中的 maxURLS 改变最大网页数量。

方式二:通过现有数据生成words.json,在awtk/tools/word_gen目录中打开终端,执行脚本to_json.js,使用现有的数据,即chinese_with_freq.txt,生成words.json文件,命令如下:

node to_json.js

chinese_with_freq.txt是从 GitHub (opens new window) 下载的,开发者可自行下载需要的联想字库配置文件,若想手动修改该文件,修改方式可参考7.9.1章节更新拼音输入法字典中修改配置文件。

(2)通过words.json生成二进制的words.bin文件,在awtk/tools/word_gen目录中打开终端,执行脚本to_words_bin.js,命令如下:

node to_words_bin.js

(3)通过生成的words_bin更新demoui的联想字库,将words_bin拷贝到demoui的资源目录,例如demos/assets/default/raw/data,然后重命名为suggest_words_zh_cn.dat。

此处以Windows平台为例,在awtk根目录下打开终端,执行以下命令:

copy tools/word_gen/words.bin demos/assets/default/raw/data/suggest_words_zh_cn.dat

(4)如果开发者在awtk/awtk_config.py文件中没有定义宏WITH_FS_RES,即不支持文件系统,那么还需要重新打包资源,在awtk根目录打开终端,执行以下命令:

python scripts/update_res.py all

AWTK默认定义宏WITH_FS_RES,即支持文件系统。

需要注意的是,基于nodejs的中文分词模块(segment)有可能出现OOM异常(内存溢出),这是nodejs安装目录下node_modules/segment/lib/module/DictTokenizer.js脚本文件中getChunks函数导致的。

如果以上遇到问题,可以通过限制chunks.length的大小解决,例如限制chunks.length为5000,代码如下:

var getChunks = function (wordpos, pos, text) {
  ...
  for (var i = 0; i < words.length; i++) {
    var word = words[i];
    var nextcur = word.c + word.w.length;
    if (!wordpos[nextcur]) {
      ret.push([word]);
    } else  {
      var chunks = getChunks(wordpos, nextcur);
      for (var j = 0; j < chunks.length && j < 5000; j++) {
        ret.push([word].concat(chunks[j]));
      }
    }
  }
  ...
  return ret;
};