前言
前面,我们已经完成了“显示系统、输入系统、文字系统、UI系统、页面系统”,我们把这些系统整合起来,就可以实现一些具体的业务了。
业务系统的主函数business\main.c
分析
int main(int argc, char **argv)
{
int error;
if (argc != 2)
{
printf("Usage: %s <font_file>\n", argv[0]);
return -1;
}
/* 初始化显示系统 */
DisplayInit();
SelectDefaultDisplay("fb");
InitDefaultDisplay();
/* 初始化输入系统 */
InputInit();
IntpuDeviceInit();
/* 初始化文字系统 */
FontsRegister();
error = SelectAndInitFont("freetype", argv[1]);
if (error)
{
printf("SelectAndInitFont err\n");
return -1;
}
/* 初始化页面系统 */
PagesRegister();
/* 运行业务系统的主页面 */
Page("main")->Run(NULL);
return 0;
}
这没啥好分析的,注释已经写得很清楚了。
配置文件的书写
为什么需要配置文件以及配置文件的示例
比如我们要显示多少个测试单元、每个单元的名字是什么、某个单元能否被点击,这些内容都可写在配置文件中,方便以后修改,示例如下:
结构体 ItemCfg
分析
typedef struct ItemCfg {
int index;
char name[100];
int bCanBeTouched;
char command[100];
}ItemCfg, *PItemCfg;
这个结构体类似用于存储每一个配置项,结合下面这幅图很容易理解各结构体的成员的意义:
C文件config\config.c
的分析
config\config.c
里主要就是解析配置文件的功能函数。当然以后可以学一种常见的配置文件的解析库,比如json文件的解析库。
全局变量g_tItemCfgs
这个是一个结构体ItemCfg类型的数组,里面有多少个成员就有多少个配置。
全局变量g_iItemCfgCount
这个用来存储统计出的一共有多少个配置项。
函数GetItemCfgCount()
int GetItemCfgCount(void)
{
return g_iItemCfgCount;
}
这个函数返回有多少个配置
函数GetItemCfgByIndext()
PItemCfg GetItemCfgByIndex(int index)
{
if (index < g_iItemCfgCount)
return &g_tItemCfgs[index];
else
return NULL;
}
这个函数通过index值返回某一配置项的具体内容。
函数 GetItemCfgByName
PItemCfg GetItemCfgByName(char *name)
{
int i;
for (i = 0; i < g_iItemCfgCount; i++)
{
if (strcmp(name, g_tItemCfgs[i].name) == 0)
return &g_tItemCfgs[i];
}
return NULL;
}
这个函数通过配置项的名字返回某一配置项的具体内容。
配置文件解析函数ParseConfigFile()
int ParseConfigFile(void)
{
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);
return -1;
}
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_tItemCfgs[g_iItemCfgCount].command[0] = '\0';
g_tItemCfgs[g_iItemCfgCount].index = g_iItemCfgCount;
sscanf(p, "%s %d %s", g_tItemCfgs[g_iItemCfgCount].name, &g_tItemCfgs[g_iItemCfgCount].bCanBeTouched, \
g_tItemCfgs[g_iItemCfgCount].command);
g_iItemCfgCount++;
}
return 0;
}
这个函数是这个C文件中其它几个文件实现的基础,也是难点。
这个函数的重点是它里面的while循环:
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_tItemCfgs[g_iItemCfgCount].command[0] = '\0';
g_tItemCfgs[g_iItemCfgCount].index = g_iItemCfgCount;
sscanf(p, "%s %d %s", g_tItemCfgs[g_iItemCfgCount].name, &g_tItemCfgs[g_iItemCfgCount].bCanBeTouched, \
g_tItemCfgs[g_iItemCfgCount].command);
g_iItemCfgCount++;
}
现在我们来分析。
关键代码fgets(buf, 100, fp)
的分析
问题:请问在这个函数中,while循环中我每次读100个字符,那每次的循环它怎么知道上一次读到哪里了?还有是怎么保证每次读取的是一行的?
答:在这个函数中,fgets
的行为是关键,它负责读取配置文件中的内容。下面详细解释:
1. fgets
的工作原理
-
函数原型:
char *fgets(char *str, int n, FILE *stream);
-
作用:
- 每次从文件指针
stream
中读取最多n-1
个字符(包括换行符\n
),并将它们存储到缓冲区str
中。 fgets
在以下任一条件满足时停止读取:- 遇到换行符
\n
。 - 达到最大字符数
n-1
。 - 遇到文件结束符(EOF)。
- 遇到换行符
- 每次从文件指针
-
内部记录位置:
- 文件指针
fp
是由fopen
打开的,它内部有一个指向文件当前位置的游标,fgets
会从当前游标开始读取数据。 - 每次调用
fgets
后,游标会自动更新到读取位置的末尾(即读取的最后一个字符的下一个位置)。
- 文件指针
2. 如何知道上一次读到哪里?
- 文件指针
fp
自动记录了文件当前的读取位置。 - 在
fgets
调用结束后,fp
的游标会更新到读取内容的末尾(如果读取到换行符,则游标移动到换行符之后)。 - 简化理解:
- 第一次调用
fgets
从文件开头读取数据。 - 第二次调用时,
fgets
会从上次结束的位置继续读取。
- 第一次调用
3. 如何保证每次读取的是一行?
fgets
的特性之一是会停止读取直到遇到换行符\n
或达到最大字符限制n-1
。- 假设配置文件的内容是:
name1 1 command1 name2 0 command2 # This is a comment name3 1 command3
- 每次调用
fgets
,会读取一整行,包括换行符。 - 即使某行超过了缓冲区大小,
fgets
也会在下一次调用中继续从剩余部分读取,直到完整读取这一行。
- 每次调用
示例运行
- 缓冲区大小:100 字符。
- 配置文件的内容中每行少于 100 字符。
fgets
一次读取完整行并在行末保留换行符,文件指针移动到换行符之后。- 例如:
- 第一次调用读取:
"name1 1 command1\n"
。 - 第二次调用读取:
"name2 0 command2\n"
。 - 遇到注释行:
"# This is a comment\n"
,这行被跳过。
- 第一次调用读取:
4. 边界情况
(1) 行的长度超过 100 字符
- 如果某行的内容长度超过缓冲区大小,
fgets
会分多次读取。 - 在上面的代码中,没有检测是否完整读取一行,可能会导致配置解析错误。
- 改进建议:
检查读取的行是否以换行符\n
结尾,如果没有,则继续从文件中读取剩余部分,直到读完整行。(下面的代码阅读本篇博文是可不看,因为配置文件的每一行绝对不会超过100个字符)while (fgets(buf, 100, fp)) { size_t len = strlen(buf); while (len > 0 && buf[len - 1] != '\n') { char temp_buf[100]; if (fgets(temp_buf, 100, fp)) { strncat(buf, temp_buf, sizeof(buf) - strlen(buf) - 1); len = strlen(buf); } else { break; } } // 继续处理完整的行数据 }
(2) 空行或注释行
- 如果某行只有空格、Tab 或注释,代码中通过以下逻辑跳过:
while (*p == ' ' || *p == '\t') p++; if (*p == '#') continue;
5. 小结
-
上次读到哪里?
文件指针fp
内部记录了读取位置,fgets
会从上次读取结束的地方继续读取。 -
如何保证每次读一行?
fgets
遇到换行符\n
或到达缓冲区限制时会停止读取。由于一般配置文件中的行不会超过 100 字符,每次都能完整读取一行。
-
需要注意的改进点:
- 增加对长行的处理逻辑,避免因行被截断导致配置解析错误。
- 检查读取到的内容是否超过缓冲区限制。
关键代码g_tItemCfgs[g_iItemCfgCount].command[0] = '\0';
由于某一个配置项中可能没有command
项,所以解析内容前先给它赋一个值。
关键代码sscanf(p, "%s %d %s", g_tItemCfgs[g_iItemCfgCount].name, ......);
的分析
这段代码通过 sscanf
函数从字符串 p
中提取三个字段并赋值到 g_tItemCfgs[g_iItemCfgCount]
结构体的对应成员中。具体如下:
1. 函数原型
sscanf
是一种从字符串中解析数据的函数:
int sscanf(const char *str, const char *format, ...);
-
参数说明:
str
: 输入字符串(从中提取数据)。format
: 格式化字符串,定义了数据解析的格式。...
: 传入的变量,用于存储提取的数据。
-
返回值:
- 成功提取的字段数量。
2. 格式化字符串 "%s %d %s"
这表示 sscanf
会按照以下规则解析字符串:
-
%s
:- 表示读取一个以空白字符(空格、Tab 或换行)为分隔符的字符串。
- 此处提取到
g_tItemCfgs[g_iItemCfgCount].name
。
-
%d
:- 表示读取一个十进制整数。
- 此处提取到
g_tItemCfgs[g_iItemCfgCount].bCanBeTouched
。
-
%s
:- 再次读取一个字符串(以空白字符为分隔符)。
- 此处提取到
g_tItemCfgs[g_iItemCfgCount].command
。
3. 目标字段
g_tItemCfgs
是一个结构体数组,其中 g_iItemCfgCount
是当前正在处理的索引。结构体字段:
name
: 存储第一个字符串。bCanBeTouched
: 存储一个整数值。command
: 存储第二个字符串。
例子分析
假设 p
中的内容为:
"button1 1 start"
运行过程
- 解析第一个字段(
%s
):- 读取
"button1"
,并将其存储到g_tItemCfgs[g_iItemCfgCount].name
。
- 读取
- 解析第二个字段(
%d
):- 读取
1
,并将其存储到g_tItemCfgs[g_iItemCfgCount].bCanBeTouched
。
- 读取
- 解析第三个字段(
%s
):- 读取
"start"
,并将其存储到g_tItemCfgs[g_iItemCfgCount].command
。
- 读取
结果
g_tItemCfgs[g_iItemCfgCount].name
="button1"
g_tItemCfgs[g_iItemCfgCount].bCanBeTouched
=1
g_tItemCfgs[g_iItemCfgCount].command
="start"
代码作用
- 该行代码将配置文件的一行内容解析为:
- 一个名称(
name
)。 - 一个布尔标志(
bCanBeTouched
,用整数 0/1 表示)。 - 一个命令字符串(
command
)。
- 一个名称(
这些值存储在全局结构体数组 g_tItemCfgs
的当前索引位置中。
小结
这段代码通过 sscanf
从配置文件的某一行提取字段,将其填充到结构体中,用于进一步处理。这种方法适合简单的字段解析,但在实际使用中需要加强输入合法性和缓冲区长度的检查以提高安全性和鲁棒性。
业务系统UI界面的生成和输入事件的处理
业务界面的主函数MainPageRun()
static void MainPageRun(void *pParams)
{
int error;
InputEvent tInputEvent;
PButton ptButton;
PDispBuff ptDispBuff = GetDisplayBuffer();
/* 读取配置文件 */
error = ParseConfigFile();
if (error)
return ;
/* 根据配置文件生成按钮、界面 */
GenerateButtons();
while (1)
{
/* 读取输入事件 */
error = GetInputEvent(&tInputEvent);
if (error)
continue;
/* 根据输入事件找到按钮 */
ptButton = GetButtonByInputEvent(&tInputEvent);
if (!ptButton)
continue;
/* 调用按钮的OnPressed函数 */
ptButton->OnPressed(ptButton, ptDispBuff, &tInputEvent);
}
}
这个函数注释已经写得比较清楚了,我们看到它里面干了几件事:
1、解析配置文件;
2、根据配置文件生成按钮的UI界面;
3、读取输入事件并根据事件找到相应的按钮,并对相应的按钮调用按钮的OnPressed函数采取相应的反就。
解析配置文件之前已经分析了,接下来就是要分析“按钮的UI界面的生成”和“输入事件的处理”
按钮的UI界面的生成
按钮的UI界面的生成需要解决的问题有哪些?
上图红框中的内容就是这个功能模块要解决的内容。
按钮生成绘制函数GenerateButtons()
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 */
g_tButtonCnt = 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 = 0.618 * width;
/* 居中显示: 计算每个按钮的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;
/* 计算每个按钮的region */
for (row = 0; (row < rows) && (i < n); row++)
{
pre_start_y = start_y + row * height;
pre_start_x = start_x - width;
for (col = 0; (col < n_per_line) && (i < n); col++)
{
pButton = &g_tButtons[i];
pButton->tRegion.iLeftUpX = pre_start_x + width;
pButton->tRegion.iLeftUpY = pre_start_y;
pButton->tRegion.iWidth = width - X_GAP;
pButton->tRegion.iHeigh = height - Y_GAP;
pre_start_x = pButton->tRegion.iLeftUpX;
/* InitButton */
InitButton(pButton, GetItemCfgByIndex(i)->name, NULL, NULL, MainPageOnPressed);
i++;
}
}
/* OnDraw */
for (i = 0; i < n; i++)
g_tButtons[i].OnDraw(&g_tButtons[i], pDispBuff);
}
这函数其实就是根据自己设计的UI界面来绘制每个按钮…注释已经把流程说得比较清楚了,所以没啥好说的…具体的按钮宽度和高度的算法参考下面这张图:
输入事件的处理
功能需求
有两种输入类型,一是点击触摸屏,二是网络客户端发来的输入数据。
根据输入事件找到按钮的函数GetButtonByInputEvent()
static PButton GetButtonByInputEvent(PInputEvent ptInputEvent)
{
int i;
char name[100];
if (ptInputEvent->iType == INPUT_TYPE_TOUCH)
{
for (i = 0; i < g_tButtonCnt; i++)
{
if (isTouchPointInRegion(ptInputEvent->iX, ptInputEvent->iY, &g_tButtons[i].tRegion))
return &g_tButtons[i];
}
}
else if (ptInputEvent->iType == INPUT_TYPE_NET)
{
sscanf(ptInputEvent->str, "%s", name);
return GetButtonByName(name);
}
else
{
return NULL;
}
return NULL;
}
这个函数的逻辑比较简单,就是根据不同的输入设备类型采取不同的判断策略,对于触摸屏输入设备,需要调用函数isTouchPointInRegion()去判断当前点击的点在哪个区域,进而判断是哪个按钮;对于网络型输入设备,则根据名字去判断是哪个按钮 。
显然,这里比较难的是函数isTouchPointInRegion()的实现。
触摸屏触点所在区域判断函数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->iHeigh)
return 0;
return 1;
}
额,这个也不难呀~
根据对应的输入事件作出反应的函数MainPageOnPressed()
static int MainPageOnPressed(struct Button *ptButton, PDispBuff ptDispBuff, PInputEvent ptInputEvent)
{
unsigned int dwColor = BUTTON_DEFAULT_COLOR;
char name[100];
char status[100];
char *strButton;
strButton = ptButton->name;
/* 1. 对于触摸屏事件 */
if (ptInputEvent->iType == INPUT_TYPE_TOUCH && ptInputEvent->iPressure != 0)
{
/* 1.1 分辨能否被点击 */
if (GetItemCfgByName(ptButton->name)->bCanBeTouched == 0)
return -1;
/* 1.2 修改颜色 */
ptButton->status = !ptButton->status;
if (ptButton->status)
dwColor = BUTTON_PRESSED_COLOR;
}
else if (ptInputEvent->iType == INPUT_TYPE_NET)
{
/* 2. 对于网络事件 */
/* 根据传入的字符串修改颜色 : wifi ok, wifi err, burn 70 */
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);
/* 居中写文字 */
DrawTextInRegionCentral(strButton, &ptButton->tRegion, BUTTON_TEXT_COLOR);
/* flush to lcd/web */
FlushDisplayRegion(&ptButton->tRegion, ptDispBuff);
return 0;
}
这段代码注释已经写得比较清楚了~
值得注意的是:
①处理触摸屏输入事件时要去判断下有没有压力值,即是不是按下,如果是放开,则不需要任何操作。
①对于网络事件,要根据status[0]的值去显示进度值,说明正在进行中,并以蓝色显示。
②这里调用了非默认的OnPressed函数,具体是在函数GenerateButtons()【这个函数在文件page\main_page.c
中】中的语句:
/* InitButton */
InitButton(pButton, GetItemCfgByIndex(i)->name, NULL, NULL, MainPageOnPressed);
来实现的。
测试
确认tslib库和freetype库的头文件
这些库的文件已经放在交叉编译器的头文件默认搜索路径中了
修改Makefile文件
略…
交叉编译
cd /home/book/mycode/C0015_business_test
make
将上面编译得到的ELF可执行文件更名为:business_test
上板测试
首先把需要的可执行文件和字库文件放于NFS文件系统中
然后把配置文件也放到NFS文件系统中
然后打开板子,把etc目录中的内容复制到板子的etc目录中。
mount -t nfs -o nolock,vers=3 192.168.5.11:/home/book/nfs_rootfs /mnt
cp -ri /mnt/business_test/etc/* /etc/
进入有可执行程序的目录:
cd /mnt/business_test/
首先把自带的QT的GUI界面关掉【这次就必须关掉了,事实证明不关掉的话会影响后面的测试】:
/etc/init.d/S99myirhmi2 stop
然后把屏幕刷黑
./draw_lcd_black
然后运行咱们的业务系统的可执行程序【后台运行】
./business_test ./simsun.ttc &
效果如下:
我点击一下LCD,发现点的时候是绿了,但马上就又返回红色了,而不像视频上那样,说明程序上有点bug,bug的解决过程见我的另一篇博文:
https://blog.youkuaiyun.com/wenhao_ir/article/details/144848294
解决bug之后的现场照片如下【上面的代码我已经进行了bug修改】:
我们再去测试一下用网络客户端发送信息进行颜色变换的功能:
现在我想让net1变绿色:
./net_client_test 127.0.0.1 "net1 ok"
效果如下:
我们再让它变回红色:
./net_client_test 127.0.0.1 "net1 err"
它就又变回红色了:
我们让burn显示进度:
./net_client_test 127.0.0.1 "net1 66"