日前,笔者在学习simplegui,制作了一套适用于simplegui的自定义字库——UTF8版。
在和群友交流的时候感觉自己讲不清楚,经过分析是自己还没能深入理解相关知识!!!
为了方便交流和提高自己,编写本文。
simplegui是一个面向单色显示屏的开源GUI接口库。作者是Polarix。目前主要在gitee上维护。源码地址:https://gitee.com/Polarix/simplegui。
作者的设计初衷是制作轻量级的仅仅用在单色屏的GUI。毕竟现在主流的GUI,比如emWin、Qt等都太过庞大。在单片机作为主控的小型项目中,有很多不能支持emWin这种GUI需要的RAM和ROM,比如笔者经常使用M0内核单片机,RAM16K,ROM120K都不能支持,更别说STM32F030系列中仅有8K的RAM,64K的ROM。
关于字库和字符编码就不详细介绍了。可以参考simplegui作者Polarix的文档https://gitee.com/Polarix/simplegui/blob/Develope/Documents/How to create font data.md 或者群友的博客https://blog.csdn.net/weixin_43614541/article/details/104581473 同时上述两个文档也都介绍了如何在simplegui中自定义字库。
理论内容看两位的文档即可,关于UTF8的实际测试。范例如下:
//文档编码格式必须是UTF8,同时代码中的汉字不乱码 #include "stdio.h" char c_chinese_buf[] = "123456请输入密码"; int main(int argc, char const *argv[]) { for (int i = 0; i < sizeof(c_chinese_buf); i++) { printf("x ", c_chinese_buf[i]); } return 0; } /** 得到结果如下: e8 af b7 请 e8 be 93 输 e5 85 a5 入 e5 af 86 密 e7 a0 81 码 00 */如果你的系统是64位,可能得到的结果是 0xffffffe8、af、0xffffffb7 这样的结果。自动忽略前置 f 即可。 如此就可以清晰的看到一个汉字(此处仅包含简单常用汉字)是由三个字节组成。
simplegui采用结构的方式调用字库。
typedef struct { SGUI_INT iHalfWidth; SGUI_INT iFullWidth; SGUI_INT iHeight; SGUI_FN_IF_GET_CHAR_INDEX fnGetIndex; SGUI_FN_IF_GET_DATA fnGetData; SGUI_FN_IF_STEP_NEXT fnStepNext; SGUI_FN_IF_IS_FULL_WIDTH fnIsFullWidth; }SGUI_FONT_RES;重点是后面四个函数的用法。我用我的语言描述下个人理解:
名称描述iHalfWidth半字宽度,一般用来显示 ASCII 字符iFullWidth全字宽度,一般用来做汉字iHeight字高度fnGetIndex获取字符索引,实际是根据指针获取以半字宽为单位的字库中的地址索引fnGetData获取字库数据,根据上一个函数获取的地址索引而计算的地址作为传入参数fnStepNext下一个字符偏移,实际是获取本字符的编码,返回下个字符的地址fnIsFullWidth判断是否全字符宽度,就是fnGetData中的那个计算方式可能比较难理解,下面有举例说明,此处不再赘述。
此处也参看上文引用的两篇文章即可。如果基础不够可以百度学习下。 我的学习经验告诉我,不是不会做字库,是不懂字符编码。
我的定义如下:
const SGUI_FONT_RES SGUI_DEFAULT_FONT_chinese = { /*SGUI_INT iHalfWidth*/ 8, /*SGUI_INT iFullWidth*/ 16, /*SGUI_INT iHeight*/ 16, /*SGUI_FN_IF_GET_CHAR_INDEX fnGetIndex*/ SGUI_Resource_GetCharIndex_chinese, /*SGUI_FN_IF_GET_DATA fnGetData*/ SGUI_Resource_GetFontData_chinese, /*SGUI_FN_IF_STEP_NEXT fnStepNext*/ SGUI_Resource_StepNext_chinese, /*SGUI_FN_IF_IS_FULL_WIDTH fnIsFullWidth*/ SGUI_Resource_IsFullWidth_chinese};后面详细介绍每个移植的细节,同时解释上文中关于这个结构体的描述。 我们不按照结构体定义的顺序介绍,而是simplegui底层库的调用顺序,准确的说是SGUI_Text_DrawText()函数的调用顺序
SGUI_Resource_StepNext_chinese() SGUI_CSZSTR SGUI_Resource_StepNext_chinese(SGUI_CSZSTR cszSrc, SGUI_UINT32 *puiCode) { /*----------------------------------*/ /* Variable Declaration */ /*----------------------------------*/ const SGUI_CHAR *pcNextChar; /*----------------------------------*/ /* Initialize */ /*----------------------------------*/ pcNextChar = cszSrc; /*----------------------------------*/ /* Process */ /*----------------------------------*/ if (NULL != pcNextChar) { //字符是ASCII if (*pcNextChar < 0x80) { *puiCode = *pcNextChar; //ASCII字符则直接赋值 pcNextChar++; //ASCII字符则直接指向下一个字符的地址 } //字符是汉字 else { //UTF8的汉字编码是三个byte表示,做一个组合表示一个汉字 *puiCode = ((*(pcNextChar + 0)) << 16) + ((*(pcNextChar + 1)) << 8) + ((*(pcNextChar + 2)) << 0); pcNextChar += 3; //此处是指向的下一个汉字的地址而不是索引值 } } return pcNextChar; } 第一个传入参数是字符指针,比如 “请输入密码” 中的 “请” 所在的地址。第二个参数是传出参数指针,这个可以自由定义,此处我把他定义为每个汉字特有的编码的组合 比如我们之前测试得到的结果:e8 af b7 请,我就使用 0x00e8afb7 表示请。 ASCII 码直接表示即可。比如 “123456” 中的 ‘1’ 直接返回 0x31 即可。函数返回值是下一个字符的地址,ASCII直接+1 即可,但是汉字字符必须+3, 因为UTF8表示的常用汉字用三个字节表示 注意:根据 SGUI_Text_DrawText()源码,此处的返回值会在下一个循环中,作为第一个传入参数使用。 SGUI_Resource_IsFullWidth_chinese() SGUI_BOOL SGUI_Resource_IsFullWidth_chinese(SGUI_UINT32 uiCode) { //字符是ASCII if (uiCode < 0x80) { return SGUI_FALSE; } //字符是汉字 else { return SGUI_TRUE; } } 传入参数就是SGUI_Resource_StepNext_chinese()的传出参数。如果字符则半宽,如果汉字则全部宽度。 SGUI_Resource_GetCharIndex_chinese() SGUI_INT SGUI_Resource_GetCharIndex_chinese(SGUI_UINT32 uiCode) { /*----------------------------------*/ /* Variable Declaration */ /*----------------------------------*/ SGUI_INT iIndex; //为了计算方便,要求字库必须是先数字后汉字,同时下面连两个数组按顺序包含全部字符 //半字宽的目录 SGUI_CBYTE c_chinese_font_index_buf_for_halfwidth[] = "0123456789"; //汉字目录 SGUI_CBYTE c_chinese_font_index_buf[] = "请输入密码老婆我爱你"; //汉字索引 SGUI_CBYTE *pc_chinese_index = NULL; /*----------------------------------*/ /* Initialize */ /*----------------------------------*/ // Initialize variable. iIndex = SGUI_INVALID_INDEX; /*----------------------------------*/ /* Process */ /*----------------------------------*/ //ASCII码 if (uiCode < 128) { pc_chinese_index = c_chinese_font_index_buf_for_halfwidth; //去除汉字,和判定结束 while ((*pc_chinese_index < 0x80) && (*pc_chinese_index != 0x00)) { if (*pc_chinese_index++ == uiCode) { iIndex = pc_chinese_index - c_chinese_font_index_buf_for_halfwidth - 1; break; //ASCII直接返回地址即可 } } } //汉字 else { pc_chinese_index = c_chinese_font_index_buf; //去除ascii while (*pc_chinese_index < 0x80) { pc_chinese_index++; } //检查数组结束 while (*pc_chinese_index > 0x7F) { if ((*pc_chinese_index++ == ((uiCode >> 16) & 0xff)) && (*pc_chinese_index++ == ((uiCode >> 8) & 0xff)) && (*pc_chinese_index++ == ((uiCode >> 0) & 0xff))) { //获取了在纯汉字中的地址偏移 iIndex = pc_chinese_index - c_chinese_font_index_buf - 3; //获取了在纯汉字中的索引,本文件是UTF8编码,汉字数组占3个byte iIndex /= 3; //汉字占用两个宽度 iIndex *= 2; //然后加上前面的ASCII的个数 iIndex += sizeof(c_chinese_font_index_buf_for_halfwidth) - 1; break; } } } return iIndex; //得到的指针偏移要除以每个汉字所占byte的长度 } 传入参数是SGUI_Resource_StepNext_chinese()的传出参数返回值是字符在字库中的位置索引,这个位置以半字宽度为一个单位。比如“12345请输入密码”, 此处“1”字位置索引是0,“2”字位置索引是1,“5”字位置索引是4, 注意“请”字的索引是5,但是“输”字的索引是7,因为“请”字是汉字,占用两个半字宽度。 同理:“入”字索引是9。本函数的代码解释: 理解 SGUI_Resource_StepNext_chinese()函数的传出参数,对本函数编程的理解有重大意义。 首先定义两个数组,分别包含半字宽的字符和汉字宽度的字符。同时要求字库内容必须先ASCII后汉字 ASCII的代码比较简单就不再赘述。 汉字的解析代码是正确比对 表示汉字的三个字节 和 字节索引数组中汉字的三个字节。 如果找到了就除以3,计算第几个汉字,然后乘以2计算第几个半字宽度索引,再加上前边的ASCII字符即可。 SGUI_Resource_GetFontData_chinese() SGUI_SIZE SGUI_Resource_GetFontData_chinese(SGUI_SIZE sStartAddr, SGUI_BYTE *pDataBuffer, SGUI_SIZE sReadSize) { SGUI_SIZE sReadCount; //TODO:考虑此处根据地址自动计算是否乘以2,以便实现字符和汉字统一数组的操作 /*貌似是库的问题,此处要乘以2*/ const SGUI_BYTE *pSrc = SGUI_FONT_chinese + sStartAddr; SGUI_BYTE *pDest = pDataBuffer; if (NULL != pDataBuffer) { for (sReadCount = 0; sReadCount < sReadSize; sReadCount++) { *pDest++ = *pSrc++; } } return sReadCount; } 这个比较简单,只是简单的复制,此处的第一个参数就是根据SGUI_Resource_StepNext_chinese()返回值计算的。这样就实现了汉字ASCII混合使用了。例如菜单中,“1.温度校准”这种使用方式。