跳到主要内容位置

业务系统

1.总结前面#

  • 前面实现了各个子系统:显示、输入、文字、UI、页面,它们只是提供基础能力,跟业务逻辑没有关系
  • 这样的架构很容易扩展,可以在这上面实现各种业务,比如可以做收银机、自动售卖机、智能称、取号机,如果再加上摄像头显示,就可以做出可视对讲、视频监控、人脸红外测温
  • 对于不同的产品,我们只需要写出自己页面函数,即PageAction结构体

2.业务系统流程#

可以写出框架,创建unittest\main.c:

int main(int argc, char **argv)
{
PDispBuff ptBuffer;
int err;
if (argc != 2)
{
printf("Usage: %s <font_file>\n", argv[0]);
return -1;
}
/* 初始化显示系统 */
DisplayInit();
SelectDefaultDisplay("fb");
InitDefaultDisplay();
ptBuffer = GetDisplayBuffer(); //获得显存buffer
/* 初始化输入系统 */
IntputRegisterInit();
IntputDeviceInit();
/* 初始化文字系统 */
FontsRegister();
err = SelectAndInitFont("freetype", argv[1]);
if (err)
{
printf("SelectAndInitFont err\n");
return -1;
}
/* 初始化页面系统 */ // UI系统不需要初始化,直接获取按键即可
PagesRegister();
/* 运行业务系统的主页面 */
Page("main")->Run();
return 0;
}

page/mainpage.c写出框架:

static void MainPageRun(void *pParams)
{
/* 读取配置文件 */
/* 根据配置文件生成按钮、界面 */
while (1)
{
/* 读取输入事件 */
/* 根据输入找到按钮 */
/* 调用按钮的OnPressed函数 */
}
}
static PageAction g_tMainPage = {
.name = "main",
.Run = MainPageRun,
};
void MainPageRegister(void)
{
RegisterPage(&g_tMainPage);
}

3.配置文件处理#

通过配置文件定义各个测试工具的项目。

先在include/config.h定义结构体、常量与函数声明:

#ifndef _CONFIG_H
#define _CONFIG_H
#define ITEMCFG_MAX_NUM 30
#define CFG_FILE "/etc/test_gui/gui.conf"
typedef struct ItemCfg
{
int index;
char name[100];
int bCanBeTouched;
char command[100];
} ItemCfg, *PItemCfg;
int GetItemCfgCount(void);
PItemCfg GetItemCfgByIndex(int index);
PItemCfg GetItemCfgByName(char *name);
#endif

实现上面三个接口函数,以及配置文件解析函数,创建config/config.c文件:

#include <stdio.h>
#include "config.h"
static ItemCfg g_atItemCfgs[ITEMCFG_MAX_NUM];
static int g_iItemCfgCount = 0;
int ParseConfigFile(char *strFileName)
{
FILE *fp;
char buf[100];
char *p = buf;
/* 1.open config file */
fp = fopen(CFG_FILE, "r");
if (!fp)
{
printf("can not open cfg file %s\n", CFG_FILE);
}
while (fgets(buf, 100, fp)) /* 2.1 read each line */
{
buf[99] = '\0';
/* 2.2 清除开头的空格或tab */
p = buf;
while (*p == ' ' || *p == '\t')
{
p++;
}
/* 2.3 忽略注释 */
if (*p == '#')
{
continue;
}
/* 2.4 处理 */
g_atItemCfgs[g_iItemCfgCount].command[0] = '\0';
g_atItemCfgs[g_iItemCfgCount].index = g_iItemCfgCount;
sscanf(p, "%s %d %s", &g_atItemCfgs[g_iItemCfgCount].name, &g_atItemCfgs[g_iItemCfgCount].bCanBeTouched,
&g_atItemCfgs[g_iItemCfgCount].command);
g_iItemCfgCount++;
}
}
int GetItemCfgCount(void)
{
return g_iItemCfgCount;
}
PItemCfg GetItemCfgByIndex(int index)
{
if (index < g_iItemCfgCount)
{
return &g_atItemCfgs[index];
}
else
{
return NULL;
}
}
PItemCfg GetItemCfgByName(char *name)
{
int i;
for (i = 0; i < g_iItemCfgCount; i++)
{
if (strcmp(name, g_atItemCfgs[i].name) == 0)
{
return &g_atItemCfgs[i];
}
}
return NULL;
}

使用fopen、fgets、sscanf方法,参考《c和指针》 15.7打开流 15.9未格式化的行IO 15.10格式化的行IO

  • fopen:打开一个特点的文件,并把一个流stream与这个文件关联 原型:

    FILE *fopen(const char *pathname, const char *mode);
  • fgets:从指定的stream读取字符串并复制到buffer中,当读取到一个换行符或者数量达到buffer_size-1时停止,且在末尾添加一个\0。下一次调用会从这个stream的下一个字符继续开始读取。 原型:

    char *fgets(char *buffer, int buffer_size, FILE *stream);
  • sscanf:从str中读取字符,并根据format格式代码对字符转进行转换。 原型:

    int sscanf(const char *str, const char *format, ...);

4.生成界面#

基本逻辑:

设计每个按钮的高度为宽度的0.618,因此按钮的面积就是width2×0.618width^2 \times0.618

总共的面积要小于屏幕面积:width2×0.618×n<=Xres×Yreswidth^2 \times 0.618 \times n <= Xres \times Yres

则宽度:width<=Xres×Yres/n/0.618width <= \sqrt{Xres \times Yres / n / 0.618}

每行可以显示的按钮数量:n_per_line=Xres/width+1n\_per\_line = Xres / width + 1 ,为防止越界,多显示一个

更新计算一下每个按钮的宽度widthwidthXres/n_per_lineXres / n\_per\_line

高度heightheightwidth×0.618width \times 0.618

编写主页面:

mainpage.c

static void MainPageRun(void *pParams)
{
int err;
/* 读取配置文件 */
err = ParseConfigFile();
if (err)
{
return err;
}
/* 根据配置文件生成按钮、界面 */
GenerateButtons();
while (1)
{
/* 读取输入事件 */
/* 根据输入找到按钮 */
/* 调用按钮的OnPressed函数 */
}
}
static PageAction g_tMainPage = {
.name = "main",
.Run = MainPageRun,
};

在写出生成按钮界面的函数GenerateButtons:

  • 计算每个按钮的width,height
  • 让整体居中,计算每个按钮的Region,保存到全局数组g_atButtons中
  • 调用按钮的显示函数,绘制出来
#define X_GAP 5
#define Y_GAP 5
static Button g_atButtons[ITEMCFG_MAX_NUM];
static void GenerateButtons(void)
{
int width, height;
int n_per_line;
int row, rows;
int col;
int n;
PDispBuff pDispBuff;
int xres, yres;
int start_x, start_y;
int pre_start_x, pre_start_y;
PButton pButton;
int i = 0;
/* 算出单个按钮的width,height */
n = GetItemCfgCount();
pDispBuff = GetDisplayBuffer();
xres = pDispBuff->iXres;
yres = pDispBuff->iYres;
width = sqrt(1.0 / 0.618 * xres * yres / n);
n_per_line = xres / width + 1;
width = xres / n_per_line;
height = width * 0.618;
/* 居中:计算每个按钮的region */
start_x = (xres - width * n_per_line) / 2;
rows = n / n_per_line;
if (rows * n_per_line < n)
rows++;
start_y = (yres - rows * height) / 2;
for (row = 0; (row < rows) && (i < n); row++) //计算region
{
pre_start_y = start_y + row * height;
for (col = 0; (col < n_per_line) && (i < n); col++)
{
pre_start_x = start_x + col * width;
pButton = g_atButtons[i];
pButton->tRegion.iLeftUpX = pre_start_x + width;
pButton->tRegion.iLeftUpY = pre_start_y + height;
pButton->tRegion.iWidth = width - X_GAP;
pButton->tRegion.iHight = height - Y_GAP;
/* InitButton */
InitButton(GetItemCfgByIndex(i)->name, pButton, NULL, NULL, NULL);
i++;
}
}
/* 显示:OnDraw */
for (i = 0; i < n; i++)
{
g_atButtons[i].OnDraw(&g_atButtons[i], pDispBuff);
}
}

5.处理输入事件#

在主页面的while循环里,获取输入事件,处理输入事件:

static void MainPageRun(void *pParams)
{
int err;
InputEvent tInputEvent;
PButton ptButton;
PDispBuff ptDispBuff = GetDisplayBuffer();
/* 读取配置文件 */
err = ParseConfigFile();
if (err)
{
return;
}
/* 根据配置文件生成按钮、界面 */
GenerateButtons();
while (1)
{
/* 读取输入事件 */
err = GetInputEvent(&tInputEvent);
if (err)
{
continue;
}
/* 根据输入找到按钮 */
ptButton = GetButtonByInputEvent(&tInputEvent);
/* 调用按钮的OnPressed函数 */
if (ptButton)
{
// printf("button %s pressed\n", ptButton->name);
ptButton->OnPressed(ptButton, ptDispBuff, &tInputEvent);
}
}
}
  • 读取输入事件:调用input的接口函数int GetInputEvent(PInputEvent PT_InputEvent);

  • 根据输入找到按钮GetButtonByInputEvent: 分为触摸事件和网络事件, 触摸事件:遍历所有按键,判断触摸在哪个按键区域,返回按键 网络事件:获取输入事件字符串,通过名字获取按键并返回。

    static PButton GetButtonByInputEvent(PInputEvent ptInputEvent)
    {
    int i;
    char name[100];
    if (ptInputEvent->iType == INPUT_TYPE_TOUCH)
    {
    for (i = 0; i < g_iButtonCnt; i++)
    {
    if (isTouchPointInRegion(ptInputEvent->iX, ptInputEvent->iY, &g_atButtons[i].tRegion))
    {
    /* debug */
    // printf("touch point is (%d, %d), tRegion sx,sy = (%d, %d) h,w = (%d, %d)\n", ptInputEvent->iX, ptInputEvent->iY,
    // g_atButtons[i].tRegion.iLeftUpX, g_atButtons[i].tRegion.iLeftUpY,
    // g_atButtons[i].tRegion.iHight, g_atButtons[i].tRegion.iWidth);
    return &g_atButtons[i];
    }
    }
    }
    else if (ptInputEvent->iType == INPUT_TYPE_NET)
    {
    sscanf(ptInputEvent->str, "%s", name);
    return GetButtonByName(name);
    }
    return NULL;
    }
    • isTouchPointInRegion:

      static int isTouchPointInRegion(int iX, int iY, PRegion ptRegion)
      {
      if (iX < ptRegion->iLeftUpX || iX >= (ptRegion->iLeftUpX + ptRegion->iWidth))
      {
      return 0;
      }
      if (iY < ptRegion->iLeftUpY || iY >= (ptRegion->iLeftUpY + ptRegion->iHight))
      {
      return 0;
      }
      return 1;
      }
    • GetButtonByName

      static PButton GetButtonByName(char *name)
      {
      int i;
      for (i = 0; i < g_iButtonCnt; i++)
      {
      if (strcmp(name, g_atButtons[i].name) == 0)
      return &g_atButtons[i];
      }
      return NULL;
      }
  • 调用按钮的OnPressed函数: 我们需要提供自己的按下函数MainPageOnPressed,先要在生成按钮界面的函数GenerateButtons中传入InitButton

    static void GenerateButtons(void)
    {
    ...
    /* 算出单个按钮的width,height */
    ...
    /* 居中:计算每个按钮的region */
    ...
    for (row = 0; (row < rows) && (i < n); row++) //计算region
    {
    cur_start_y = start_y + row * height;
    // pre_start_x = start_x - width;
    for (col = 0; (col < n_per_line) && (i < n); col++)
    {
    ...
    /* InitButton */
    InitButton(GetItemCfgByIndex(i)->name, pButton, NULL, NULL, MainPageOnPressed);
    i++;
    }
    }
    /* 显示:OnDraw */
    ...
    }

    然后在MainPageOnPressed中修改按键状态:

    1. 对于触摸屏事件

      1.1 分辨能否被点击

      1.2 修改颜色

    2. 对于网络事件

      2.1 根据传入字符串修改颜色

    3. 其余事件无法处理

    4. 绘制底色,居中写文字,flush to lcd or web

    static int MainPageOnPressed(struct Button *ptButton, PDispBuff ptDispBuff, PInputEvent ptInputEvent)
    {
    unsigned int dwColor = BUTTON_DEFAULT_COLOR;
    char name[100];
    char status[100];
    char *strButton = ptButton->name;
    /* 1. 对于触摸屏事件 */
    if (ptInputEvent->iType == INPUT_TYPE_TOUCH && ptInputEvent->iPressure == 0) // 松开的时候响应
    {
    /* 1.1 分辨能否被点击 */
    if (0 == GetItemCfgByName(ptButton->name)->bCanBeTouched)
    {
    return -1;
    }
    /* 1.2 修改颜色 */
    ptButton->status = !ptButton->status;
    if (1 == ptButton->status)
    {
    dwColor = BUTTON_PRESSED_COLOR;
    }
    }
    /* 2. 对于网络事件 */
    else if (ptInputEvent->iType == INPUT_TYPE_NET)
    {
    /* 2.1 根据传入字符串修改颜色 */
    sscanf(ptInputEvent->str, "%s %s", name, status);
    if (strcmp(status, "ok") == 0)
    {
    dwColor = BUTTON_PRESSED_COLOR;
    }
    else if (strcmp(status, "err") == 0)
    {
    dwColor = BUTTON_DEFAULT_COLOR;
    }
    else if (status[0] >= '0' && status[0] <= '9')
    {
    dwColor = BUTTON_PERCENT_COLOR;
    strButton = status;
    }
    else
    {
    return -1;
    }
    }
    else
    {
    return -1; //其余输入事件无法处理
    }
    //绘制底色
    DrawRegion(&ptButton->tRegion, dwColor);
    //居中写文字
    DrawTextInRegionCentrl(strButton, &ptButton->tRegion, BUTTON_TEXT_COLOR);
    // flush to lcd or web
    FlushDispalyRegion(&ptButton->tRegion, ptDispBuff);
    return 0;
    }

6.综合测试#

修改makefile,添加各个子目录的makefile,

修改顶层目录makfile

obj-y += display/
obj-y += input/
obj-y += font/
obj-y += ui/
obj-y += page/
obj-y += business/
obj-y += config/

链接错误:

/home/book/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin/../lib/gcc/arm-buildroot-linux-gnueabihf/7.5.0/../../../../arm-buildroot-linux-gnueabihf/bin/ld: built-in.o: undefined reference to symbol 'sqrt@@GLIBC_2.4'

这个错误是链接器(ld)在链接过程中发生的问题,提示缺少对 sqrt 函数的引用。这通常是由于数学库(libm)未正确链接导致的。

在顶层目录Makefile中添加-lm,-lm 是指示链接器链接 libm.so,这是数学库的动态链接版本。

LDFLAGS := -lts -lpthread -lfreetype -lm

计算居中位置出错:

我的方法是x,y都是通过行列计算当前的位置,而韦东山老师的方法y的坐标是通过行计算当前坐标,x的坐标是通过更新的方式。

/* 居中:计算每个按钮的region */
start_x = (xres - width * n_per_line) / 2;
rows = n / n_per_line;
if (rows * n_per_line < n)
rows++;
start_y = (yres - rows * height) / 2;
for (row = 0; (row < rows) && (i < n); row++) //计算region
{
cur_start_y = start_y + row * height;
// pre_start_x = start_x - width;
for (col = 0; (col < n_per_line) && (i < n); col++)
{
cur_start_x = start_x + col * width;
pButton = &g_atButtons[i];
pButton->tRegion.iLeftUpX = cur_start_x;
pButton->tRegion.iLeftUpY = cur_start_y;
pButton->tRegion.iWidth = width - X_GAP;
pButton->tRegion.iHight = height - Y_GAP;
// pre_start_x = pButton->tRegion.iLeftUpX;
/* InitButton */
InitButton(GetItemCfgByIndex(i)->name, pButton, NULL, NULL, MainPageOnPressed);
i++;
}
}

注意strcmp,两个字符串相等返回0:

static PButton GetButtonByName(char *name)
{
int i;
for (i = 0; i < g_iButtonCnt; i++)
{
if (strcmp(name, g_atButtons[i].name) == 0)
return &g_atButtons[i];
}
return NULL;

测试:

[root@imx6ull:/mnt/31_improve_command]# ./client 127.0.0.1 "net1 ok"
Get Msg From 127.0.0.1 : net1 ok

请点击左侧菜单(移动端为右下角)选择要查看的所有笔记吧。