为什么要分析字符串的显示过程?
学习uCGUI主要是学习如何使用的,为何要深究到源码的层次呢?
就分析字符串显示过程的原因来说,是因为移植汉字字库的需要。uCGUI并么有合适的汉字字库,而且完整的汉字字库非常庞大,消耗单片机的Flash资源。如果想要移植一个合适的字库,分析字符串显示的过程以及uCGUI字库数据结构,还是很有必要的。
GUI_DispString()函数源码
1 void GUI_DispString(const char GUI_UNI_PTR *s) { 2 int xAdjust, yAdjust, xOrg; 3 int FontSizeY; 4 if (!s) 5 return; 6 GUI_LOCK(); 7 FontSizeY = GUI_GetFontDistY(); //获取字体的高度 8 xOrg = GUI_Context.DispPosX; //获取当前显示的x坐标 9 /* Adjust vertical position */ 10 yAdjust = GUI_GetYAdjust(); 11 GUI_Context.DispPosY -= yAdjust; //根据Y方向上的对齐方式对y进行调整 12 for (; *s; s++) { 13 GUI_RECT r; 14 int LineNumChars = GUI__GetLineNumChars(s, 0x7fff); //当前一行要显示几个字符 15 int xLineSize = GUI__GetLineDistX(s, LineNumChars); //当前一行在x方向上的像素数 16 /* Check if x-position needs to be changed due to h-alignment */ 17 switch (GUI_Context.TextAlign & GUI_TA_HORIZONTAL) { 18 case GUI_TA_CENTER: xAdjust = xLineSize / 2; break; 19 case GUI_TA_RIGHT: xAdjust = xLineSize; break; 20 default: xAdjust = 0; 21 } 22 /* 计算出每一行显示内容的矩形区域 */ 23 r.x0 = GUI_Context.DispPosX -= xAdjust; //根据水平方向的对齐方式对x坐标进行调整 24 r.x1 = r.x0 + xLineSize - 1; 25 r.y0 = GUI_Context.DispPosY; 26 r.y1 = r.y0 + FontSizeY - 1; 27 28 GUI__DispLine(s, LineNumChars, &r); //以计算好的矩形区域显示当前行字符 29 GUI_Context.DispPosY = r.y0; 30 s += GUI_UC__NumChars2NumBytes(s, LineNumChars); //从第一个字符开始,地址加1,可以遍历整行字符串 31 if ((*s == '\n') || (*s == '\r')) { 32 switch (GUI_Context.TextAlign & GUI_TA_HORIZONTAL) { 33 case GUI_TA_CENTER: 34 case GUI_TA_RIGHT: 35 GUI_Context.DispPosX = xOrg; 36 break; 37 default: 38 GUI_Context.DispPosX = GUI_Context.LBorder; 39 break; 40 } 41 if (*s == '\n') 42 GUI_Context.DispPosY += FontSizeY; 43 } else { 44 GUI_Context.DispPosX = r.x0 + xLineSize; 45 } 46 if (*s == 0) /* end of string (last line) reached ? */ 47 break; 48 } 49 GUI_Context.DispPosY += yAdjust; // 50 GUI_Context.TextAlign &= ~GUI_TA_HORIZONTAL; // 51 GUI_UNLOCK(); 52 }
字符串显示过程概括
<1> 获取选择字体的高度、宽度
<2> 从所传字符串参数中,依次读出一行的字符数,最终得到一行显示所对应的矩形区域
<3> 将一行所得到的详细信息传给行显示函数,行显示函数会从字库中找到匹配的字依次将一行的字符进行显示,完成一行的显示
<4> 如果不是只有一行,重新计算下一行显示的坐标,重复<1、2、3>的工作,直到将字符串显示完毕。
============================================================================================
重要细节分析
============================================================================================
1、GUI运行的全局变量
GUI_Context是GUI保存运行环境的全局变量,它的类型GUI_CONTEXT在GUI.h中被定义。
struct GUI_CONTEXT { /* Variables in LCD module */ LCD_COLORINDEX_UNION LCD; LCD_RECT ClipRect; U8 DrawMode; U8 SelLayer; U8 TextStyle; /* Variables in GL module */ GUI_RECT* pClipRect_HL; /* High level clip rectangle ... Speed optimization so drawing routines can optimize */ U8 PenSize; U8 PenShape; U8 LineStyle; U8 FillStyle; /* Variables in GUICHAR module */ const GUI_FONT GUI_UNI_PTR * pAFont; //指向当前选择的字体 #if GUI_SUPPORT_UNICODE const GUI_UC_ENC_APILIST * pUC_API; /* Unicode encoding API */ #endif I16P LBorder; I16P DispPosX, DispPosY; I16P DrawPosX, DrawPosY; I16P TextMode, TextAlign; //对齐方式 GUI_COLOR Color, BkColor; /* Required only when changing devices and for speed opt (caching) */ /* Variables in WM module */ #if GUI_WINSUPPORT const GUI_RECT* WM__pUserClipRect; GUI_HWIN hAWin; //当前激活的窗口 int xOff, yOff; #endif /* Variables in MEMDEV module (with memory devices only) */ #if GUI_SUPPORT_DEVICES const tLCDDEV_APIList* pDeviceAPI; /* function pointers only */ GUI_HMEM hDevData; GUI_RECT ClipRectPrev; #endif /* Variables in Anitaliasing module */ #if GUI_SUPPORT_AA const tLCD_HL_APIList* pLCD_HL; /* Required to reroute drawing (HLine & Pixel) to the AA module */ U8 AA_Factor; U8 AA_HiResEnable; #endif };
2、GUI_FONT的定义
一种字库想要被uCGUI所调用,需要将其定义成GUI_GONT类型的一个常量,当我们需要自己制作字库的时候,就需要这样做。GUI_FONT类型的定义在GUIType.h文件中。
struct GUI_FONT { GUI_DISPCHAR* pfDispChar; //显示一个属于当前字库字符的函数 GUI_GETCHARDISTX* pfGetCharDistX; //获取字库中某字符的宽度 GUI_GETFONTINFO* pfGetFontInfo; //获取字库信息 GUI_ISINFONT* pfIsInFont; //查询字库中是否存在此字符 const tGUI_ENC_APIList* pafEncode; // U8 YSize; //高度 U8 YDist; //对应的像素点 U8 XMag; //X方向上的放大系数 U8 YMag; //Y方向上的放大系数 union { //此共用体主要是提供字库数据的访问地址, //对于不同的字库,其从字库中查找字模的方法,是不一样的 //这里主要分成三种情况,对应三种类型的指针 const void GUI_UNI_PTR * pFontData; const GUI_FONT_MONO GUI_UNI_PTR * pMono; const GUI_FONT_PROP GUI_UNI_PTR * pProp; } p; U8 Baseline; // U8 LHeight; /* height of a small lower case character (a,x) */ //小写高度 U8 CHeight; /* height of a small upper case character (A,X) */ //大写高度 };
(1)单一分区的字库,例如GUI_Font6x8,使用的共用体指针是第二个
/* MONO字库代表的是单一分区 */ typedef struct { const unsigned char GUI_UNI_PTR * pData; //字库的起始地址 const U8 GUI_UNI_PTR * pTransData; // const GUI_FONT_TRANSINFO GUI_UNI_PTR * pTrans; // U16P FirstChar; //第一个字符的索引 U16P LastChar; //最后一个字符的索引 U8 XSize; //宽度 U8 XDist; //宽度对应的像素点 U8 BytesPerLine; //每行需要几个字节 } GUI_FONT_MONO;
这类字库的特点:
① 字符不多,对应的字模也很少,不会很浪费Flash
② 之所以是一个分区,那是因为所有的字符的索引码都是连续的,所以根据字符索引寻找字符的字模时,从字库的基地址开始找起就可以了。
(2)多个分区的字库,比如汉字的字库,使用的共用体指针是第三个
typedef struct GUI_FONT_PROP { U16P First; /* first character */ U16P Last; /* last character */ const GUI_CHARINFO GUI_UNI_PTR * paCharInfo; /* address of first character */ const struct GUI_FONT_PROP GUI_UNI_PTR * pNext; /* pointer to next */ } GUI_FONT_PROP;
相较(1)结构体中的内容,GUI_FONT_PROP显得少了几个。由于字库是分区管理,每一个分区有自己特有的参数,通过paCharInfo指针来指向这个分区的特点,也会给出这个分区的第一个字符字模的地址。
typedef struct { U8 XSize; //字符的宽度 U8 XDist; //显示所对应的像素点 U8 BytesPerLine; //每行占据的字节数 const unsigned char GUI_UNI_PTR * pData; //指向字模的数据区 } GUI_CHARINFO;
可能有人会说:不使用GUI,将汉字字库下载到SD卡中,然后使用下边的函数GetGBKCode_from_sd()也可以获得字模,而且这种字库所有的分区都是连续排放的。为什么要与上一种情况单独分开呢?
/******************************************************************************* * Function Name : GetGBKCode_from_sd * Description : 从SD卡字库中读取自摸数据到指定的缓冲区 * Input : pBuffer---数据保存地址 * c--汉字字符低字节码 * Output : None * Return : 0(success) -1(fail) * Attention : None *******************************************************************************/ int GetGBKCode_from_sd(unsigned char* pBuffer,const unsigned char * c) { unsigned char High8bit,Low8bit; unsigned int pos; High8bit=*c; /* 取高8位数据 */ Low8bit=*(c+1); /* 取低8位数据 */ pos = ((High8bit-0xa0-16)*94+Low8bit-0xa0-1)*2*16; f_mount(0, &myfs[0]); myres = f_open(&myfsrc , "0:/HZLIB.bin", FA_OPEN_EXISTING | FA_READ); if ( myres == FR_OK ) { f_lseek (&myfsrc, pos); //指针偏移 myres = f_read( &myfsrc, pBuffer, 32, &mybr ); //16*16大小的汉字 其字模 占用16*2个字节 f_close(&myfsrc); return 0; } else return -1; }
虽然字库是连续存放的,但是根据汉字机内码从这样的字库中找到对应的字模存放的位置,这样的算法是不通用的。不通用的意思是,uCGUI支持多种外语,可它不可能为每一种外语都写这样的算法,那样的代码不具包容性。
正确的做法是考虑到多种外语的共同点,字符索引都可以看做是分区排列的,而且在每个分区中都是连续的。每个分区中查找字模的算法肯定是一样的,而在创建字库的时候,将字模分区管理。
这种做法最为重要的意义在于:可以裁剪字库,自然节省了Flash的消耗。16*16点阵的汉字字库,它的大小是200多kB,对于小容量Flash的单片机来说难以承受。有人说可以外置SD卡,可是实际的项目中怎么可能将字库放在SD卡中,怎么可能就为了字库而添加SD卡这样的硬件资源,而且SD卡的读速度跟内部Flash是不可比的。
为了减少ROM的消耗,裁剪字库是最为有效的方法。裁剪字库的基础就是将字库进行分区管理,一个分区甚至可以只有一个汉字。将字库进行了分割,但是所有的分区有都属于同一个字库,这就需要链表将它们连接起来。于是乎,就产生了第三种共用体指针。
3、属于字库的特有的函数
struct GUI_FONT { GUI_DISPCHAR* pfDispChar; //显示一个属于当前字库字符的函数 GUI_GETCHARDISTX* pfGetCharDistX; //获取字库中某字符的宽度 GUI_GETFONTINFO* pfGetFontInfo; //获取字库信息 GUI_ISINFONT* pfIsInFont; //查询字库中是否存在此字符 ...... };
不同的字库不仅有属于自己的数据库,而且还有自己特有的函数,这也是由于其特别的因素所决定的。
4、构建自己需要的字库时需要的工作
英文ASCII的字库,uCGUI提供的已经非常完备了,通常无需多虑。而uCGUI所欠缺的是属于我们中国的汉字字库。
庞大的字库使我们只有选择“const GUI_FONT_PROP GUI_UNI_PTR * pProp;”这种办法才是光明之道。
我们需要将自己需要显示的汉字字模分区建立GUI_FONT_PROP类型的结构体,然后使用指针pProp将它们连接成单项链表构成一个完整的字库。这样字库的数据区就做好了。
还需要为字库创建诸多查询功能的管理函数,例如16*16点阵的汉字字库可以选用
#define GUI_FONTTYPE_PROP_SJIS \ GUIPROP_DispChar, \ GUIPROP_GetCharDistX, \ GUIPROP_GetFontInfo, \ GUIPROP_IsInFont, \ &GUI_ENC_APIList_SJIS
显然上边的这些函数都是基于链表的,有什么样的数据结构就有什么样的算法的一种体现。
对于只有一个分区的MONO英文字库,是不需要用链表的,只需要在数组中根据偏移量查找就行了。