单片机 GUI 设计(六)- 行输入控件
单片机 GUI 设计(六)- 行输入控件

单片机 GUI 设计(六)- 行输入控件

基于 GD32F303ZET6 苹果派开发板

简介

行输入是一种输入控件,用于获取用户输入的一行字符串,应用广泛,浏览器的搜索栏、一些小工具的参数设置,等等。行输入控件通常有两种状态,分别是有焦点和无焦点。写入时只能写入有焦点的行输入控件,不能写入没有焦点的行输入控件;读取时不论控件是否有焦点,都能读取成功。

行输入控件通常与按键输入控件搭配使用,实现基本的用户交互。

控制结构体设计

行输入控件具有“有焦点”和“无焦点”两种状态,通过点击改变状态。点击一个行输入控件后,如果该控件没有焦点,那么会自动获取焦点。此时,如果界面内有其它行输入控件具有焦点,那么就要取消该控件的焦点,保证界面内只能有一个行输入控件有焦点。行输入控件的状态是通过点击改变的,与按键类似,因此行输入控件的控制结构体可以参考按键控件,如下所示。

//行输入控件控制结构体
typedef struct
{
  u16   x, y, width, height;  //原点、宽度、高度
  char* text;                 //输入结果,创建时自动动态分配
  u8    textSize;             //字体大小,可以是 12、16、24
  u16   textColor;            //字体颜色
  u8    focus;                //0-无焦点,1-有焦点
  u16   hasFocusColor;        //有焦点时显示的颜色,没有背景图片时使用
  u16   noFocusColor;         //无焦点时显示的颜色,没有背景图片时使用
  void* hasFocusImage;        //有焦点时显示的图片,使用 JPEG 图片,不用时请填入 NULL
  void* noFocusImage;         //无焦点时显示的图片,使用 JPEG 图片,不用时请填入 NULL
}StructLineEdit;

API 函数设计

行输入控件所需要的 API 接口函数比较多,具体如下所示。通过 LineEditScan 函数扫描行输入控件时,如果检测到控件有被按下,那么控件将自动获得焦点并更新状态。对于多个行输入控件,通常只有一个拥有焦点,所以需要用 LineEditRemoveFocus 函数清除行输入控件的焦点状态。

void CreateLineEdit(StructLineEdit* widget);         //创建行编辑控件
u32  LineEditScan(StructLineEdit* widget);           //行编辑控件扫描
void LineEditRemoveFocus(StructLineEdit* widget);    //取消行编辑控件的焦点
void LineEditAdd(StructLineEdit* widget, char code); //行编辑控件添加一个字符
void LineEditDelete(StructLineEdit* widget);         //删除一个字符

驱动源文件包含的头文件

驱动文件需要包含的头文件如下所示,为了美观,我们可以用 JPEG 图片来装饰行输入控件。

#include "LineEdit.h"
#include "LCD.h"
#include "Touch.h"
#include "JPEG.h"
#include "stdio.h"

创建控件函数

在创建控件函数中,我们要设置行输入控件的初始状态,同时还要为行输入控件的结果输入准备一个缓冲区,用于储存用户输入,具体如下所示。一般行输入控件都会有黑色或白色边框,为了显示的字符不会覆盖到边框,本驱动中行输入的首位都预留了一个空格的位置。

void CreateLineEdit(StructLineEdit* widget)
{
  //行输入控件能容纳的最大字符数量
  unsigned int maxStringLen;
  
  //计算行编辑控件最大字符串长度
  maxStringLen = widget->width / (widget->textSize / 2);
  
  //为字符串缓冲区申请动态内存
  widget->text = (char*)malloc(maxStringLen + 1);
  if(NULL == widget->text)
  {
    printf("Fail to malloc\r\n");
    while(1){}
  }
  
  //设定第一个字符为空格
  widget->text[0] = ' ';
  widget->text[1] = 0;
  
  //默认无焦点
  widget->focus = 0;
  
  //更新显示
  UpdateShow(widget);
}

刷新显示函数

上述代码中的 UpdateShow 函数用于刷新行输入控件的显示,可以做为一个内部函数,如下所示。UpdateShow 函数首先刷新背景,可以是刷图或矩形填充。因为行输入控件通常是矩形的,为了节省内存空间,在这里将行输入控件的图片类型定为 JPEG 图片。刷新完背景后,可以按照纵坐标居中的方式打印行输入控件输入结果。

static void UpdateShow(StructLineEdit* widget)
{
  unsigned int x0, y0, x1, y1, i, x, y;
  
  //计算计算控件坐标范围
  x0 = widget->x;
  y0 = widget->y;
  x1 = widget->x + widget->width - 1;
  y1 = widget->y + widget->height - 1;
  
  //有焦点时刷新背景
  if(1 == widget->focus)
  {
    if(NULL != widget->hasFocusImage)
    {
      DrawJPEG(widget->hasFocusImage, widget->x, widget->y);
    }
    else
    {
      LCDFill(x0, y0, x1, y1, widget->hasFocusColor);
    }
  }
  
  //无焦点时刷新背景
  else
  {
    if(NULL != widget->noFocusImage)
    {
      DrawJPEG(widget->noFocusImage, widget->x, widget->y);
    }
    else
    {
      LCDFill(x0, y0, x1, y1, widget->noFocusColor);
    }
  }
  
  //计算字符串起点的横坐标和纵坐标
  x0 = widget->x;
  y0 = widget->y + ((widget->height - widget->textSize) / 2);
  
  //显示字符串
  i = 0;
  x = x0;
  y = y0;
  while(0 != widget->text[i])
  {
    LCDShowChar(x, y, widget->text[i], widget->textSize, 1);
    x = x + (widget->textSize / 2);
    i++;
  }
}

扫描函数

行输入控件的扫描函数如下所示,一旦确定行输入按键有被按下,就可以直接将焦点标志位设为 1 ,最后调用 UpdateShow 更新显示即可。这里的返回值只是为了通知用户行输入控件是否有状态改变,用户可以据此解除其它行输入控件的焦点状态。

u32 LineEditScan(StructLineEdit* widget)
{
  unsigned int x0, y0, x1, y1;
  unsigned short tx, ty;
  
  //已经获得焦点,直接返回
  if(0 != widget->focus)
  {
    return 0;
  }
  
  //计算控件坐标范围
  x0 = widget->x;
  y0 = widget->y;
  x1 = widget->x + widget->width - 1;
  y1 = widget->y + widget->height - 1;
  
  //获取触屏扫描结果
  if(0 != ScanTouch(&tx, &ty))
  {
    //检测到控件被按下
    if((tx >= x0) && (tx <= x1) && (ty >= y0) && (ty <= y1))
    {
      //标记获得了焦点
      widget->focus = 1;
      
      //刷新显示
      UpdateShow(widget);
      
      //返回状态改变了
      return 1;
    }
  }
  
  //返回状态未改变
  return 0;
}

清除焦点函数

清除行输入控件的焦点状态更为简单,只需要清空标志位,然后刷新显示即可。

void LineEditRemoveFocus(StructLineEdit* widget)
{
  //当前并未获得焦点,直接返回
  if(0 == widget->focus)
  {
    return;
  }
  
  //标记未获得焦点
  widget->focus = 0;
  
  //刷新显示
  UpdateShow(widget);
}

输入函数

LineEditAdd 函数用于向行输入控件添加一个输入,具体定义如下所示。需要注意:行输入控件的首位两端需要预留至少一个空格的空白区域,以免字符显示覆盖到了边框。

void LineEditAdd(StructLineEdit* widget, char code)
{
  unsigned int maxStringLen, stringLen;
  
  //当前并未获得焦点,直接返回
  if(0 == widget->focus)
  {
    return;
  }
  
  //非法字符,直接返回
  if(code < ' ')
  {
    return;
  }
  
  //计算行编辑控件最大字符串长度
  maxStringLen = widget->width / (widget->textSize / 2);
  maxStringLen = maxStringLen - 1;
  
  //统计当前字符串长度
  stringLen = 0;
  while(0 != widget->text[stringLen])
  {
    stringLen++;
  }
  
  //字符串长度大于等于最大字符串长度,直接返回
  if(stringLen >= maxStringLen)
  {
    return;
  }
  
  //将字符添加到字符串末尾
  widget->text[stringLen] = code;
  widget->text[stringLen + 1] = 0;
  
  //刷新显示
  UpdateShow(widget);
}

删除函数

LineEditDelete 函数用于从末尾删除行输入控件的输入结果,具体定义如下所示。在这里要注意不要误删了最前边预留的空格。

void LineEditDelete(StructLineEdit* widget)
{
  unsigned int stringLen;
  
  //当前并未获得焦点,直接返回
  if(0 == widget->focus)
  {
    return;
  }
  
  //统计字符串长度
  stringLen = 0;
  while(0 != widget->text[stringLen])
  {
    stringLen++;
  }
  
  //只有一个字符,直接返回,用于保护最前边的空格
  if(1 == stringLen)
  {
    return;
  }
  
  //删除末尾的字符
  widget->text[stringLen - 1] = 0;
  
  //刷新显示
  UpdateShow(widget);
}

测试代码

行输入控件的测试代码如下,因为这里所有的控件都需要做触屏扫描,以前的触屏扫描函数每调用一次都会触发 IIC 传输,效率极低,所以代码工程中做了优化,新添了触屏扫描任务,用于每隔一段时间做一次触屏扫描。

#include "Button.h"
#include "LineEdit.h"

int main(void)
{
  //行输入控件
  static StructLineEdit s_structLineEdit1;
  static StructLineEdit s_structLineEdit2;
  
  //按键控件
  static StructButton s_structButton1Widget;
  static StructButton s_structButton2Widget;
  static StructButton s_structButton3Widget;
  static StructButton s_structButton4Widget;
  
  //初始化
  InitHardware();   //初始化硬件相关
  InitSoftware();   //初始化软件相关

  //设置 LCD 初始状态
  LCDDisplayDir(1);
  LCDClear(WHITE);
  
  //创建行输入 1
  s_structLineEdit1.x = 150;
  s_structLineEdit1.y = 150;
  s_structLineEdit1.width = 475;
  s_structLineEdit1.height = 50;
  s_structLineEdit1.textColor = WHITE;
  s_structLineEdit1.textSize = 24;
  s_structLineEdit1.hasFocusColor = GREEN;
  s_structLineEdit1.noFocusColor = BLUE;
  s_structLineEdit1.hasFocusImage = NULL;
  s_structLineEdit1.noFocusImage = NULL;
  CreateLineEdit(&s_structLineEdit1);
  
  //创建行输入 2
  s_structLineEdit2.x = s_structLineEdit1.x;
  s_structLineEdit2.y = s_structLineEdit1.y + s_structLineEdit1.height + 25;
  s_structLineEdit2.width = s_structLineEdit1.width;
  s_structLineEdit2.height = s_structLineEdit1.height;
  s_structLineEdit2.textColor = s_structLineEdit1.textColor;
  s_structLineEdit2.textSize = s_structLineEdit1.textSize;
  s_structLineEdit2.hasFocusColor = s_structLineEdit1.hasFocusColor;
  s_structLineEdit2.noFocusColor = s_structLineEdit1.noFocusColor;
  s_structLineEdit2.hasFocusImage = s_structLineEdit1.hasFocusImage;
  s_structLineEdit2.noFocusImage = s_structLineEdit1.noFocusImage;
  CreateLineEdit(&s_structLineEdit2);
  
  //按键 1 初始化
  s_structButton1Widget.x = 150;
  s_structButton1Widget.y = 340;
  s_structButton1Widget.width = 100;
  s_structButton1Widget.height = 50;
  s_structButton1Widget.text = "1";
  s_structButton1Widget.textColor = WHITE;
  s_structButton1Widget.textSize = 24;
  s_structButton1Widget.pressColor = GREEN;
  s_structButton1Widget.releaseColor = BLUE;
  s_structButton1Widget.pressImage = NULL;
  s_structButton1Widget.releaseImage = NULL;
  ButtonCreate(&s_structButton1Widget);
  
  //按键 2 初始化
  s_structButton2Widget.x = s_structButton1Widget.x + s_structButton1Widget.width + 25;
  s_structButton2Widget.y = s_structButton1Widget.y;
  s_structButton2Widget.width = s_structButton1Widget.width;
  s_structButton2Widget.height = s_structButton1Widget.height;
  s_structButton2Widget.text = "2";
  s_structButton2Widget.textColor = s_structButton1Widget.textColor;
  s_structButton2Widget.textSize = s_structButton1Widget.textSize;
  s_structButton2Widget.pressColor = s_structButton1Widget.pressColor;
  s_structButton2Widget.releaseColor = s_structButton1Widget.releaseColor;
  s_structButton2Widget.pressImage = s_structButton1Widget.pressImage;
  s_structButton2Widget.releaseImage = s_structButton1Widget.releaseImage;
  ButtonCreate(&s_structButton2Widget);
  
  //按键 3 初始化
  s_structButton3Widget.x = s_structButton2Widget.x + s_structButton2Widget.width + 25;
  s_structButton3Widget.y = s_structButton1Widget.y;
  s_structButton3Widget.width = s_structButton1Widget.width;
  s_structButton3Widget.height = s_structButton1Widget.height;
  s_structButton3Widget.text = "3";
  s_structButton3Widget.textColor = s_structButton1Widget.textColor;
  s_structButton3Widget.textSize = s_structButton1Widget.textSize;
  s_structButton3Widget.pressColor = s_structButton1Widget.pressColor;
  s_structButton3Widget.releaseColor = s_structButton1Widget.releaseColor;
  s_structButton3Widget.pressImage = s_structButton1Widget.pressImage;
  s_structButton3Widget.releaseImage = s_structButton1Widget.releaseImage;
  ButtonCreate(&s_structButton3Widget);
  
  //按键 BACK 初始化
  s_structButton4Widget.x = s_structButton3Widget.x + s_structButton3Widget.width + 25;
  s_structButton4Widget.y = s_structButton1Widget.y;
  s_structButton4Widget.width = s_structButton1Widget.width;
  s_structButton4Widget.height = s_structButton1Widget.height;
  s_structButton4Widget.text = "<-";
  s_structButton4Widget.textColor = s_structButton1Widget.textColor;
  s_structButton4Widget.textSize = s_structButton1Widget.textSize;
  s_structButton4Widget.pressColor = s_structButton1Widget.pressColor;
  s_structButton4Widget.releaseColor = s_structButton1Widget.releaseColor;
  s_structButton4Widget.pressImage = s_structButton1Widget.pressImage;
  s_structButton4Widget.releaseImage = s_structButton1Widget.releaseImage;
  ButtonCreate(&s_structButton4Widget);

  while(1)
  {
    //LED 闪烁
    LEDFlicker(10);
    
    //触屏扫描
    TouchScanTask();
    
    //行输入 1 扫描
    if(LineEditScan(&s_structLineEdit1))
    {
      LineEditRemoveFocus(&s_structLineEdit2);
    }
    
    //行输入 2 扫描
    if(LineEditScan(&s_structLineEdit2))
    {
      LineEditRemoveFocus(&s_structLineEdit1);
    }
    
    //按键 1 扫描
    if(ButtonScan(&s_structButton1Widget))
    {
      LineEditAdd(&s_structLineEdit1, '1');
      LineEditAdd(&s_structLineEdit2, '1');
    }
    
    //按键 2 扫描
    if(ButtonScan(&s_structButton2Widget))
    {
      LineEditAdd(&s_structLineEdit1, '2');
      LineEditAdd(&s_structLineEdit2, '2');
    }
    
    //按键 3 扫描
    if(ButtonScan(&s_structButton3Widget))
    {
      LineEditAdd(&s_structLineEdit1, '3');
      LineEditAdd(&s_structLineEdit2, '3');
    }
    
    //按键 BACK 扫描
    if(ButtonScan(&s_structButton4Widget))
    {
      LineEditDelete(&s_structLineEdit1);
      LineEditDelete(&s_structLineEdit2);
    }
    
    //延时 50 ms
    DelayNms(50);
  }
}

实验结果

实验结果如下所示。

总结

我们可以用链表的形式组织行输入控件,为行输入控件分组。这样一来,当一个行输入控件获得焦点后,另一个将自动失去焦点,就不用用户自己去管理每个行输入控件的状态了。同时,往行输入控件写入或删除时,可以直接针对控件分组进行。在写入/删除函数中,遍历链表,查找出具有焦点的控件,并更新状态,这样用户就无需多次调用写入/删除函数了。

源码

本章节中的源码请参考《单片机 GUI 设计(零)- 大纲