之前一直是看别人的博客,但是一直没有写过,这里的主要原因是本人文笔本来就比较差,以前作文就只能勉强及格,更重要的是本人技术还没有达到那个层次,在我自己把某个东西弄懂之前不敢妄自发文,否则那便是自以为是。但是现在发现做完的东西没有一点记录也不行,起码也要留点东西证明你是做技术的才行啊,就像人死了要立块碑写上墓志铭。
做了4年技术今天决定第一次写一篇技术博客。写技术博客的目的一来是做下总结和反思,同时也方便以后进行查阅,二来给自己的职业留点纪念。之前查看过很多技术博客,有时候需要查阅相关资料,百度一搜(无奈谷歌无法用)很多技术博客都是一模一样的,很多人都是随意转。当然好的博客是值得转,但是转载之前请先看明白了,不说将之补充,起码也要修改了其中的错误再转吧,不然就是以讹传讹。
既然决定写就得认真对待每一篇技术文章,哪怕是转载。
Ucgui的字符显示功能模块由4部分组成:
1、字体管理结构:
这个结构体记录当前的字体信息,是字库管理模块的入口,包含字符显示函数指针和字体信息操作函数指针,这里我们只要关注pfDispChar这个字符显示函数就行了,其他的我也没仔细看。这里我只对要用到的相关成员进行注释而已。
struct GUI_FONT {
GUI_DISPCHAR* pfDispChar; //字符显示调用函数
GUI_GETCHARDISTX* pfGetCharDistX;
GUI_GETFONTINFO* pfGetFontInfo;
GUI_ISINFONT* pfIsInFont;
const tGUI_ENC_APIList* pafEncode;
U8 YSize; //字体高度
U8 YDist;
char XMag;
char YMag;
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) */
};
这个结构体的地址通过函数GUI_SetFont(const GUI_FONT GUI_UNI_PTR *pFont)记录在
GUI_Context.pAFont中,函数void GUIPROP_DispChar(U16P c)就是通过这个成员来查找到字符信息节点,进而通过字符信息节点找到对应的字符点阵数据进行显示的。具体怎么查找之后再分析。对于软件我们一定要有分层次的思想,先了解架构,再熟悉层次,最后再分析细节,不然对于大型程序很容易迷失在代码中。
2、字符信息查找管理链表节点结构:
这是字符信息查找管理链表节点结构,由于所有字符的unicode并不是连续的,所以链表中每个节点只能对应一段或者叫一块字符的信息表,即整个字符信息表的一块。字符信息表结构下面会讲到,通过遍历这个链表可以查找到对应字符的信息节点。
typedef struct GUI_FONT_PROP {
U16P First; //对应字符信息表的第一个字符的unicode码
U16P Last; //对应字符信息表的最后一个字符的unicode码
const GUI_CHARINFO GUI_UNI_PTR * paCharInfo; /*对应字符信息表的第一个字符信息节点地址*/
const struct GUI_FONT_PROP GUI_UNI_PTR * pNext; /*下一个节点指针,通过它组成链表*/
} GUI_FONT_PROP;
3、字符信息表节点结构:
每个字符对应一个字符信息表中的一项。
typedef struct {
U8 XSize; //字符宽度
U8 XDist;//x方向字符间距
U8 BytesPerLine; //字符点阵每行字节数
const unsigned char GUI_UNI_PTR * pData; //字符点阵数据所在地址
} GUI_CHARINFO;
4、点阵数据数组:
由相关工具生成的对应字体的点阵字库数据,每个字符的数据存在一个unsigned char数组中,将数组的地址保存到字符信息表对应项的const unsigned char GUI_UNI_PTR * pData成员中。
字符通过void GUIPROP_DispChar(U16P c)显示,参数为字符unicode码。函数源码如下:
void GUIPROP_DispChar(U16P c)
{
int BytesPerLine;
GUI_DRAWMODE DrawMode = GUI_Context.TextMode;
/*查找字符信息管理节点,下面会对这个函数进行解析*/
ConstGUI_FONT_PROPGUI_UNI_PTR*pProp= GUIPROP_FindChar(GUI_Context.pAFont->p.pProp,c);
if (pProp) {
GUI_DRAWMODE OldDrawMode;
//通过字符信息管理节点找到对应的字符信息节点
const GUI_CHARINFO GUI_UNI_PTR * pCharInfo = pProp->paCharInfo+(c-pProp->First);
BytesPerLine = pCharInfo->BytesPerLine;
OldDrawMode = LCD_SetDrawMode(DrawMode);
LCD_DrawBitmap( GUI_Context.DispPosX, GUI_Context.DispPosY,
pCharInfo->XSize,
GUI_Context.pAFont->YSize,
GUI_Context.pAFont->XMag,
GUI_Context.pAFont->YMag,
1, /* Bits per Pixel */
BytesPerLine,
pCharInfo->pData,
&LCD_BKCOLORINDEX
);
/* Fill empty pixel lines */
if (GUI_Context.pAFont->YDist > GUI_Context.pAFont->YSize) {
int YMag = GUI_Context.pAFont->YMag;
int YDist = GUI_Context.pAFont->YDist * YMag;
int YSize = GUI_Context.pAFont->YSize * YMag;
if (DrawMode != LCD_DRAWMODE_TRANS) {
LCD_COLOR OldColor = GUI_GetColor();
GUI_SetColor(GUI_GetBkColor());
LCD_FillRect(GUI_Context.DispPosX,
GUI_Context.DispPosY + YSize,
GUI_Context.DispPosX + pCharInfo->XSize,
GUI_Context.DispPosY + YDist);
GUI_SetColor(OldColor);
}
}
LCD_SetDrawMode(OldDrawMode); /* Restore draw mode */
GUI_Context.DispPosX += pCharInfo->XDist * GUI_Context.pAFont->XMag;
}
}
//参数pProp:链表头 c:对应字符unicode码
static const GUI_FONT_PROP GUI_UNI_PTR * GUIPROP_FindChar(
const GUI_FONT_PROP GUI_UNI_PTR* pProp,
U16P c)
{
for (; pProp; pProp = pProp->pNext)
{
if ((c>=pProp->First) && (c<=pProp->Last))
{
break;
}
}
return pProp;
}
通过遍历字符信息管理链表找到对应字符所在管理节点,从而通过字符信息管理节点和字符相对信息表第一项的偏移找到对应的字符信息节点。
const GUI_CHARINFO GUI_UNI_PTR * pCharInfo = pProp->paCharInfo+(c-pProp->First);
pProp->paCharInfo:字符所在信息表首地址
(c-pProp->First):对应字符相对首地址的偏移
字符信息节点找到后也就找到了对应字符的点阵数据和相关信息。
字库文件存储管理
如果像PC内存足够大,可以将字库信息和点阵直接编译到内存,这样可以直接使用。但是当内存空间不够时就只能将字库信息存到外存(E2PROM、flash、 sd)。这样当改变显示字体时只需要更换字库信息相关文件而不需要修改程序。
字库信息分成3部分:字符信息查询管理链表、字符信息表、点阵数据。我们创建3个文件来分别存储这3部分信息。
重新封装C语言库函数中的文件读写函数:
int FILE_Write(FILE*fd, const void* buf, int len)
int FILE_Read(FILE*fd, const void*buf, int len)
针对不同的存储介质和文件系统需要采用对应的数据操作函数接口。
创建字符信息查询管理链表文件:
文件存储结构:
typedef struct{
U16P First; /* first character */
U16P Last; /* last character */
U16P offset; // 首个字符信息节点数据在字符信息表文件中的偏移 ,这个偏移是//字符信息表的项数,而不是字节数
}PROP_NODE_t;
U8 CreatePropFile(const char *fileName)
{
const GUI_FONT_PROP* pProp;
PROP_NODE_t propNode;
U16P preCnt=0,preOff=0;
FILE *fd;
U8 len = sizeof(PROP_NODE_t);
const GUI_FONT *pFont=GUI_GetFont();
fd = fopen(fileName,"wb+");
if(fd <= NULL)
{
return 0;
}
//将字符信息管理链表写到文件
for(pProp = pFont->p.pProp; pProp; pProp=(const GUI_FONT_PROP*) pProp->pNext)
{
propNode.First = pProp->First;
propNode.Last = pProp->Last;
propNode.offset = preCnt+preOff;
preCnt = propNode.Last-propNode.First+1;
preOff = propNode.offset;
if(len != FILE_Write(fd,&propNode,len))
{
fclose(fd);
return 0;
}
}
fclose(fd);
return 1;
}
创建字符信息表文件:
文件存储结构:
typedef struct{
U8 XSize;
U8 XDist;
U8 BytesPerLine;
U32 offset;//对应字符的点阵数据首地址在点阵字库文件中的偏移字节数
}CHAR_INFO_t;
U8 CreateCharInfoFile(const char *fileName,const GUI_CHARINFO charInfo[], U32 cnt)
{
U32 i;
FILE *fd;
U8 lastLen=0;
CHAR_INFO_t tmpNode;
U8 len = sizeof(CHAR_INFO_t);
const GUI_FONT *pFont=GUI_GetFont();
memset(&tmpNode,0,len);
fd = fopen(fileName,"wb+");
if(fd <= NULL)
{
return 0;
}
for(i = 0; i < cnt; i++)
{
tmpNode.XSize = charInfo[i].XSize;
tmpNode.XDist = charInfo[i].XDist;
tmpNode.BytesPerLine = charInfo[i].BytesPerLine;
(tmpNode.offset) += lastLen;
lastLen = (charInfo[i].BytesPerLine)*(pFont->YSize);
if(FILE_Write(fd,&tmpNode,len) != len)
{
fclose(fd);
return 0;
}
}
fclose(fd);
return 1;
}
创建字符点阵数据文件:
这个文件存储字符最终显示的点阵数据,非等宽字库每个字符的点阵数据大小不同,多以没有固定的文件存储格式。
U8 CreateDotDataFile(const char* fileName,const GUI_CHARINFO *charInfo, U32 cnt)
{
U32 i;
FILE *fd;
U8 len=0;
const GUI_FONT *pFont=GUI_GetFont();
fd = fopen(fileName,"wb+");
if(fd <= NULL)
{
return 0;
}
for(i = 0;i<cnt;i++)
{
len = (charInfo[i].BytesPerLine)*(pFont->YSize);
if(len != FILE_Write(fd,charInfo[i].pData,len))
{
fclose(fd);
return 0;
}
}
fclose(fd);
return 1;
}
字库信息文件创建完成了,下一步要通过读取相关文件找到对应字符的信息节点和点阵数据。我们要做的就是将之前在内存中查找字符信息和点阵数据修改成从文件中查找。
需修改以下函数
1、static const GUI_FONT_PROP GUI_UNI_PTR * GUIPROP_FindChar(const GUI_FONT_PROP GUI_UNI_PTR* pProp, U16P c)
//定义两个变量
static GUI_FONT_PROP fontProp; //存储字符信息管理节点
static GUI_CHARINFO charInfo; //存储字符信息
static const GUI_FONT_PROP GUI_UNI_PTR * GUIPROP_FindChar(const GUI_FONT_PROP GUI_UNI_PTR* pProp, U16P c)
{
static char buf[100];
PROP_NODE_t propNode;
CHAR_INFO_t charInfoNode;
U32 offset;
const GUI_CHARINFO GUI_UNI_PTR * pCharInfo;
const GUI_FONT *pFont=GUI_GetFont();
if(ReadProp(c,&propNode)) //读取字符信息管理节点
{
offset = propNode.offset+(c-propNode.First);
if(ReadCharInfo(offset,&charInfoNode)) //读取字符信息节点
{
memset(buf,0,sizeof(buf));
//读取字符点阵数据
if(ReadDotData(charInfoNode.offset, (charInfoNode.BytesPerLine)*(pFont->YSize),
buf))
{
/*将字符信息节点和字符信息管理节点保存到变量*/
charInfo.XSize = charInfoNode.XSize;
charInfo.XDist = charInfoNode.XDist;
charInfo.BytesPerLine = charInfoNode.BytesPerLine;
charInfo.pData = buf;
fontProp.First = propNode.First;
fontProp.Last = propNode.Last;
fontProp.paCharInfo = &charInfo;
return &fontProp;
}
}
}
return NULL;
}
对应文件读取函数后面会分析到。
2、由于内存中不存在字符信息表,不能通过pProp->paCharInfo+(c-pProp->First)找到对应字符信息节点,需要将void GUIPROP_DispChar(U16P c)函数中的
const GUI_CHARINFO GUI_UNI_PTR * pCharInfo = pProp->paCharInfo+(c-pProp->First);
替换成:const GUI_CHARINFO GUI_UNI_PTR * pCharInfo = &charInfo;
3、由于以上原因,所以也要修改函数:
int GUIPROP_GetCharDistX(U16P c)
{
ConstGUI_FONT_PROP GUI_UNI_PTR * pProp = GUIPROP_FindChar(GUI_Context.pAFont->p.pProp, c);
//return (pProp)? (pProp->paCharInfo+(c-pProp->First))->XSize*GUI_Context.pAFont->XMag :0;
return (pProp) ? (charInfo.XSize) * GUI_Context.pAFont->XMag : 0;
}
读取字符信息管理节点函数
U8 ReadProp(U16P c,PROP_NODE_t *pProp)
{
FILE *fd;
U8 len = sizeof(PROP_NODE_t);
const char *fileName;
const GUI_FONT *pFont=GUI_GetFont();
//根据字体信息找到对应的文件名,我这里有两种字体
if(&GUI_Font_Arial_Narrow18x22 == pFont)
{
fileName = ARI_NARROW_18X22_PROP_PATH;
}
else
{
fileName = ARI_NARROW_14X17_PROP_PATH;
}
fd = fopen(fileName,"rb");
if(fd <= NULL)
{
return 0;
}
while(!feof(fd))
{
if(len != FILE_Read(fd,pProp,len))
{
fclose(fd);
return 0;
}
if(ferror(fd))
{
fclose(fd);
return 0;
}
if((c >= pProp->First) && (c <= pProp->Last))
{
fclose(fd);
return 1;
}
}
fclose(fd);
return 0;
}
读取字符信息文件
U8 ReadCharInfo(U32 offset,CHAR_INFO_t *buf)
{
U8 len = sizeof(CHAR_INFO_t);
FILE *fd;
const char *fileName;
U32 off = offset*len;
//根据字体信息找到对应的文件名,我这里有两种字体
if(&GUI_Font_Arial_Narrow18x22 == GUI_GetFont())
{
fileName = ARI_NARROW_18X22_INFO_PATH;
}
else
{
fileName = ARI_NARROW_14X17_INFO_PATH;
}
fd = fopen(fileName,"rb");
if(fd <= NULL)
{
return 0;
}
if(off != fseek(fd,off,SEEK_SET))
{
/*fseek 并不是返回当前文件的偏移位置,这里没有做文件偏移出错处理
可以重写一个偏移函数实现
*/
//fclose(fd);
//return 0;
}
if(len != FILE_Read(fd,buf,len))
{
fclose(fd);
return 0;
}
fclose(fd);
return 1;
}
读取点阵数据
U8 ReadDotData(U32 offset, U8 len,char *buf)
{
FILE *fd;
const char *fileName;
//根据字体信息找到对应的文件名,我这里有两种字体
if(&GUI_Font_Arial_Narrow18x22 == GUI_GetFont())
{
fileName = ARI_NARROW_18X22_DATA_PATH;
}
else
{
fileName = ARI_NARROW_14X17_DATA_PATH;
}
fd = fopen(fileName,"rb");
if(fd <= NULL)
{
return 0;
}
if(offset != fseek(fd,offset,SEEK_SET))
{
//fclose(fd);
//return 0;
}
if(len != FILE_Read(fd,buf,len))
{
fclose(fd);
return 0;
}
fclose(fd);
return 1;
}
以上功能是在PC上实现的,对应不同的平台修改文件操作接口即可。
显示字符串存储管理
将每种语言的显示字符存储都对应的文件,用户配置语言是将对应的显示字符串读到内存中。
显示字符串在内存中的存储结构:
typedef struct{
STR_ID_t id; //字符串Id号,每个Id对应一条字符串,此值是枚举类型
char *str; //字符串指针
}LANGUAGE_NODE_t;
由于每条字符串的长度不同,不好采用固定的结构存储字符串。以前在一本书上看到:任何软件问题都可以通过增加一个中间层来解决,我套用下:任何文件存储问题都可以增加一个中间文件来解决。这个思想在字库村粗管理中也用到了,所以这里用另外一个字符串信息文件记录每条字符串的长度,字符串依次存放在另一个字符串文件中。
创建字符串信息文件:
实际就是将每个字符串的长度存储起来
字符串文件信息表节点结构
typedef struct{
//STR_ID_t id;
INT8U len; //字符串长度
//INT16U offset;//字符串在文件中的偏移位置
}TR_INFO_t;
这里我只是用到了字符串的长度,如果字符串没有从文件映射到内存的话,我们需要从文件中查找对应的字符串,这时可以增加其他信息,通过相关信息从文件中读取字符串。
void CreateTrInfoTable(const char *fileName,LANGUAGE_NODE_t *pLanguage)
{
TR_INFO_t trInfo;
STR_ID_t id;
INT8U len=sizeof(TR_INFO_t);
FILE *fd;
fd = fopen(fileName,"wb+");
if(fd <= NULL)
{
return;
}
for(id=NULL_ID;id<STR_ID_BUF;id++)
{
//trInfo.id = engLanguage[id].id;
trInfo.len = strlen(pLanguage[id].eng);
//trInfo.offset = preLen+preOff;
//preLen = trInfo.len;
//preOff = trInfo.offset;
fwrite((const char *)&trInfo,len,1,fd);
}
fclose(fd);
}
创建字符串存储文件:
void CreateStrFile(const char *fileName,LANGUAGE_NODE_t *pLanguage)
{
STR_ID_t id;
FILE *fd;
char rollEnd[]={0x0d};
fd = fopen(fileName,"wb+");
if(fd <= NULL)
{
return;
}
for(id=NULL_ID;id<STR_ID_BUF;id++)
{
fwrite((const char *)pLanguage[id].eng,strlen(pLanguage[id].eng),1,fd);
fwrite((const char *)rollEnd,1,1,fd);//存储换行符,方便查看字符串文件,也可以不要
}
fclose(fd);
}
将字符串映射到内存显示字符串数组
字符串在内存中的存储数组,STR_ID_BUF为字符串id总数。
static LANGUAGE_NODE_t strNodeList[STR_ID_BUF];
配置语言时需要调用此函数,同时需要将原字符串的内存存储空间释放掉。
释放原字符串的内存空间:
static void InitStrNodeList(void)
{
STR_ID_t id;
for(id=NULL_ID;id<STR_ID_BUF;id++)
{
strNodeList[id].id = id;
if(strNodeList[id].eng)
{
free(strNodeList[id].eng);
strNodeList[id].eng = NULL;
}
}
}
void ReadStrFile2Ram(const char* infoFileName,const char* textFileName)
{
FILE *infoFd;
FILE *textFd;
TR_INFO_t trInfo;
STR_ID_t id;
INT8U len=sizeof(TR_INFO_t);
char tmp;
InitStrNodeList();
infoFd = fopen(infoFileName,"rb");
textFd = fopen(textFileName,"rb");
if(infoFd <= NULL || textFd <= NULL)
{
return;
}
for(id=NULL_ID;id<STR_ID_BUF;id++)
{
fread((char *)&trInfo,len,1,infoFd);
strNodeList[id].eng = (char *)malloc(trInfo.len+1);
memset(strNodeList[id].eng,0,trInfo.len+1);
fread(strNodeList[id].eng,trInfo.len,1,textFd);
/*如果创建字符串存储文件的时候每个字符串后添加
了换行符这里需要将换行符进行偏移*/
fread((char *)&tmp,1,1,textFd);
}
fclose(infoFd);
fclose(textFd);
}
显示字符串查找函数
char * TR(STR_ID_t id)
{
STR_ID_t i;
LANGUAGE_NODE_t *language = strNodeList;
if(id >= STR_ID_BUF || id <NULL_ID)
{
return NULL;
}
for(i=NULL_ID;i<STR_ID_BUF;i++)
{
if(language[i].id == id)
{
return language[i].str;
}
}
return NULL;
}
文本用到的字库生成工具下载地址:ucgui字库生成工具-C工具类资源-CSDN下载