LCUI 初始版本开发日志

发表于2011年10月08日

LCUI 0.10.0 至 0.12.0

LCUI 大约是在2011年10月份开始开发的,起初是仿照VC的工具箱中的窗口控件,添加了PictureBox部件、Label部件,这些部件的相关属性及命名,也借鉴了它。
头文件的编写,我先仿照了FreeType2的头文件编写风格,也就是先#include <XXX_Build.h>,要使用哪个头文件里的函数声明时,直接使用 #include _XX_XX_H_,这个宏是 XXX_Build.h 里定义的;但遇到了一些问题,头文件包含头文件,循环包含,最终编译器编译时一直在循环打印同一个错误,关于这个问题,解决方法是使用宏进行头文件保护;

在后来,发现有嵌套关系的结构体如果定义在其它头文件里,编译器会报错;调试了一段时间,决定直接将结构体及相关数据的定义写在一个头文件里,其余头文件写函数声明;在解决结构体嵌套问题时,对gtk感到好奇:它是如何解决头文件相关的问题的?于是参考了gtk的头文件,把我的头文件中的结构体的定义方式稍微改了一下,但也只是改了一下而已。

图形界面,字体显示是少不了的,相关文档以及例子很少,只有自己看FreeType源码目录里的文档,借助Google翻译来了解大致意思,光看也没用,需要代码例子来参考,网上的示例代码比较少,大多是清一色的获取单个文字字形的示例代码,文字对齐问题没解决(如下图所示)。

hellworld

FreeType获取字体字形需要该字体对应的字符的unicode编码,而源代码中使用的是char型字符串,不是wchar_t型,这需要实现一个功能:将char型字符串内的字符编码转换成unicode编码并保存至wchar_t型字符串。我用了iconv库提供的相关函数实现了字符编码转换。

图形的显示,不再是利用mgaview程序源码中的write_to_fb函数显示图形,借鉴了它的代码,自己重新写了一个函数,可以用这个函数往屏幕写内容。

随后,实现了PictureBox部件,图片的缩放功能还未添加,添加后,可以进行图片的放大和缩小的操作。

由于PictureBox部件的实现,我又想做个照片查看器,于是,第一个版本的照片查看器就诞生了。

use_lcpv_1use_lcpv_2

use_lcpv_3use_lcpv_4

该版本的照片查看器使用了Label部件在窗口内的左上角显示图片信息,2秒后,逐渐减少Label部件的alpha值,直到为0(完全透明),方向键移动浏览范围,i键调出图片信息标签(不透明)。还不能放大和缩小图片,因为没有加入图像缩放算法,PictureBox部件无法对图片使用图像缩放算法。

以上是老版本的LCUI库的开发过程,没做过多的说明。

LCUI 0.12.1 至 0.12.2

修改了数据类型,Label部件、PictureBox部件等,都改成用LCUI_Widget结构体储存,而不是之前那样每种部件使用不同的结构体。

由于数据结构体的改变,各个函数就需要进行相应的修改,数据的处理方式也要改,代码修改量比较大,花费了几天的时间来测试、纠错、完善,终于完成了。

Label部件的字体处理做了优化,频繁打开关闭字体文件来获取每个字的位图,效率很低,于是我将字体相关的数据用结构体保存,字体文件的句柄也保存在结构体中;第一次打开字体文件,获取完字体后,不会关闭字体文件,这是为了在下一次获取字体位图时能够跳过打开文件这个步骤,节省时间。

局部刷新机制也做了修改,针对Label部件的文本内容的变动,先遍历原有的文本与新的文本内容进行对比,如果有改动,就重绘这个字体,并将这个字体所在的区域记录下来,等待局部刷新之,比起之前的处理方式,效率相对来说提高了一些,至少不会因为改动几个字或者新增几个字而全部在屏幕上重绘。

窗口还是矩形窗口,想过实现圆角窗口,根据圆的方程,写了个矩形圆角化的算法,可是,边缘没有线条,理论上圆角化的同时会绘制边缘线,实际上并没看到边缘线,暂时先不管,还是继续编写主要功能的代码。

一个图形界面,鼠标操作是少不了的,源代码比较好找,但找到的代码只能获取鼠标左右键的状态,不能获取鼠标滚轮的状态,以后需要这个时再找解决方法。

既然支持鼠标,我就加了个基本功能:拖动窗口移动,为了实现局部刷新,又写了个算法,为了解决如下图所示的问题:

window_move

根据矩形的坐标、尺寸以及移动至的新位置的坐标,得出需要刷新的两个矩形A、B,刷新这两个区域的图形就可以抹去残余图形。

在将窗口移动至屏幕以外的区域时,显示会出问题,于是,将图形写入至帧缓冲时要考虑到是否超出显示范围,超出范围的就进行裁剪。

实现了LCUI_Key_Event_Connect函数,原型为:
int LCUI_Key_Event_Connect(LCUI_Window win_p, int key_value, void (*func)(void), void *arg);

主要功能就是将对应键值的按键产生的事件与函数关联,当按下被设定的键值对应的按键后,触发相应事件,LCUI_Main函数就会调用与该事件关联的函数,并将之前保存的参数传过去。参数只能是一个void*类型的变量,和pthread_create函数原型类似,想要多个不同类型的参数,就需要自己写个结构体,包含这些变量,之后转换成void*型,在需关联的函数里,把void*型参数转换成原来的类型。

还有个LCUI_Widget_Event_Connect函数,原型为:
int LCUI_Widget_Event_Connect(LCUI_Widget widget, int event, void (*func)(void), void *arg);

这个是将某个窗口部件产生的事件与函数关联,例如:
将Exit_LCUI函数与按钮部件的clicked事件关联,当鼠标左键点击这个按钮部件并松开后,就会调用Exit_LCUI函数,关闭窗口,并退出LCUI。

功能是这样的,但相应的处理功能没完成,窗口没有添加关闭按钮,不能靠鼠标关闭窗口。

这也算是模拟实现了Qt的connect函数的一些功能,至少能满足现在的需求。

需要有一个字体数据队列来保存已打开的字体文件,避免重复打开同一字体文件。

要有队列处理函数,用于处理窗口显示顺序队列、部件显示顺序队列,具备新增、删除队列成员的功能。

过了一段时间,完成了Button部件,可以在窗口中使用按钮。完成了LCUI_Widget_Event_Connect函数,LCUI_Main函数中,添加了对部件事件的处理。

test_button

中途修正了部分代码的逻辑错误,纠正了队列处理函数出现的错误,这个队列处理函数,用于处理程序窗口的显示顺序、窗口内部件的显示顺序以及部件事件注册、部件触发的事件,虽然每种队列的队列成员类型不一样,但大致的处理代码是一样的。

现在的按钮绘制得很简陋,但大致的处理框架已经完成,想要好一点的按钮,只需要调整一下按钮绘制函数绘制的图形内容即可。

Label部件支持彩色文本显示,但不是很好用,需要指定文字的具体位置,才能让该文字使用其它颜色,需要完善。

部件共有6种事件:

事件类型  说明
Normal    部件由其它状态切换至普通状态时,才会触发该事件。
Clicked   当部件被点击,并且在该部件上释放按键,该事件会被触发。
Overlay   当鼠标覆盖在该部件上的时候,该事件被触发。
Down      部件被鼠标点击,但按键未被释放,处于按住状态,该事件被触发。
Focus     部件处于焦点状态,该事件被触发。(有待完善,有的时候可以和其它状态重叠)
Disable   部件未被启用,该事件被触发。

但支持的只有前4种。

Button部件在创建的时候就已经注册了这6个事件,与这些事件关联的是Update_Widget函数,更新按钮时,会跟据部件的状态来绘制相应的部件图形。

手动设定部件在窗口中的位置还真麻烦,看了一下GTK和QT的程序代码,有个水平布局盒子(HBox)和垂直布局盒子(VBox),用来调整部件在窗口中的布局的,如果窗口尺寸改变,部件的布局也跟着改变,说到窗口尺寸改变,正考虑是添加窗口尺寸改变的事件处理,还是在调用相应函数改变窗口尺寸时自动处理布局,这个布局盒子,可以被其它布局盒子包含,也可以包含窗口部件,至于处理方法,想得很纠结,算了,等以后实在是需要这个时再花时间来想吧。

个人认为如果每种部件都写相应的处理函数,会变得很麻烦,代码编写量也多,这些部件应该需要有统一的处理函数,方便代码维护,也减少了代码编写量,于是,整理出了一些设计思路:

每个部件创建之初都是透明的。

Label部件如果没有背景图,那么字体位图中不透明的像素点对应部件中的像素点也不透明。如果有背景图,那透明度为完全不透明,先填充背景色再混合背景图,根据背景图的布局来做相应的处理,例如:拉伸、缩放、居中、平铺,最后粘贴字体位图。除了背景图,还有一个图层,这个图层也可以显示图像,但没有背景图那样的处理,只是根据对齐方式来调整图像的粘贴位置,该图层与部件图形混合的时,如果没有背景图,就和粘贴文字位图那样。

更新该部件时的处理流程为:
开始-》判断是否有背景图-》有就让部件不透明,否则透明-》判断是否有要显示的图像-》有的话,根据图像对齐方式来调整图像位置,之后根据图像的alpha通道而局部改变部件的alpha通道-》粘贴文字位图-》结束。

PictureBox部件,和Label部件一样,如果没有背景图,那么就全透明。如果有要显示的图像,那么,部件图形数据中的alpha通道会根据图像的alpha通道而局部改变,更新该部件时的处理流程为:
开始-》判断是否有背景图-》有就让部件不透明,否则透明-》判断是否有要显示的图像-》有的话,根据图像处理模式来处理图像,之后根据图像的alpha通道而局部改变部件的alpha通道-》结束。

Button部件,和上面几个部件一样,但是可以设定风格,有默认风格和自定义风格,默认风格就是根据部件的状态,绘制不同颜色的背景图,加上边框;而自定义,就是自己指定按钮在不同状态时所使用的图形,切换状态时,就会使用自己定义的按钮图形,默认对图形进行拉伸处理,如果没有,就会使用默认的按钮图形,更新该部件时的处理顺序为:
开始-》判断是否有背景图-》有就让部件不透明,否则透明-》判断按钮风格-》默认风格则绘制简陋按钮图形,自定义风格则使用自定义的图形-》判断是否有要显示的图像-》有的话,根据图像的对齐方式以及与文本的关系,来处理图像,否则不处理-》根据字体对齐方式以及与图像的关系,在对应的位置粘贴字体-》结束。

根据以上思路,得到这些部件处理的共同处,合并了一些代码,写了Update_Widget函数,用于更新部件,先处理部件背景图,之后根据不同的部件类型来调用相应的函数以完成部件的更新。

不久,又添加了部件布局功能,用Set_Widget_Align函数可设定指定部件的布局,总共有9种布局方式:左上、中上、右上、中左、中间、中右、左下、中下、右下,并支持偏移坐标,每次改变窗口尺寸时,自动根据每个部件的布局方式以及偏移坐标得出位置并设定,有的时候并不需要完全是这个布局,比如:某个部件要在右下角,但是,要与边缘距离5个像素点,也就是向左移动5个像素点,向上移动5个像素点,那么,就可以用偏移坐标了,当offset_x和offset_y为负数时,就会在这个以通过布局方式计算出的xy坐标中加上这个偏移值,由于是负数,就相当于减去了。

写了一个石头剪刀布的游戏,用到了Label部件、PictureBox部件以及button部件,也用到了事件关联和部件布局功能,具体请看图:

mora1mora2

在测试游戏期间,纠正了事件处理、部件图形更新处理、字体位图处理、局部图形刷新处理等功能的代码所出现的问题。

编写了一个图片水平翻转函数,如上图所示(第二张),同一张图片显示成水平对称的两张图片。

完善了窗口标题栏刷新功能,窗口尺寸被改变后,标题栏会被重绘,在标题栏上的关闭按钮也会被刷新。

几天后,加入了触屏的支持,用了tslib提供的示例程序的代码,窗口右上角也添加了关闭按钮(如下图所示)。

mora3mora4

在调试触屏功能的过程中,纠正了部件图形刷新功能,因为有两种部件:一种在标题栏,一种在客户区,两种部件的图形刷新要分开处理,混在一起的话,会出现刷新问题。

完善了触屏点击处理,之前测试时,点击一下按钮,只变成了下凹的状态,没有触发部件被点击的事件,部件状态更改的函数的代码也做了相应更改,因为这问题是这函数的问题所导致的。

还发现Label部件在改变尺寸并移动位置后,存在残余图形,它的更新方式就需要改动:如果有一行文本内容中的一个被字改动,就刷新该字后面一整行的字,单个字进行局部刷新不准确,会有残余图形。

添加一个变量,保存部件位置的类型,用于指示部件是在标题栏还是在用户区,绘制标题栏按钮需要用到。

有了触屏支持,当然要有一个触屏校准程序,于是就修改tslib源码目录里的test程序的源码,改成LCUI版的,调试了几遍,最终完成了,可以看这图:

ts_calibrate

窗口风格为无(NONE),不会绘制窗口标题栏,那个“点击圆圈中心,笔点校正”(Label部件)本来是显示在中间的,可是测试时,发现圆圈(PictureBox部件)移动到中央的过程中,留下了Label部件残余图形,这问题暂时先放着,因此,就把Label部件的位置调整到圆圈不会覆盖到的位置。

写照片查看器的时候,发现我写的这个LCUI还有蛮多问题的,为部件添加背景图后,段错误,不添加的话,Label部件中的文字有时会不完整,打开图片文件后,有时会出现部件排列顺序出错,本来在底层的部件居然显示在其它部件之上,刷新也是,不仅如此,段错误居然不是必然事件了,同一个操作,重复多次,有几次不同的结果,这几个不同的结果就是刚刚所说的,这怎么找BUG?悲剧了。

思考了一下,感觉应该是没有进行数据保护,之前储存图形数据的结构体没有加入互斥锁的功能,导致频繁切换图形显示时直接出现段错误,因为我的这个GUI的实现用了多线程,每个窗口的图形数据是共享的,如果一个线程在使用free函数释放一个内存空间的同时,另一个线程又对这个内存空间进行读取数据的操作,那么就会出现段错误。在加了数据保护后,问题解决了,其实就是加了个变量,表示该结构体中的指针是否正在被使用,是的话,用usleep函数等待,直到没有被用为止。

经过几天的奋斗,问题终于解决了,修改了主循环的图形数据处理功能,之前的label部件的问题,是由于对部件更新队列的处理不当,而不是因为数据同步问题,在更新label部件中的文本时,如果开启了自动调整大小,那么就会在生成文字位图后重新计算尺寸,之后就会调用相关函数来调整部件尺寸,但是调整部件的尺寸需要再次对部件进行更新,更新队列默认是不添加重复的部件指针的,于是使用了一个函数来强制将这个部件加入至更新队列,这样问题就解决了。

编写了0.12版的照片查看器,在电脑端测试正常,而在学习机里,程序居然卡住了,在主循环中加上printf函数来打印信息,学习机上的测试结果表明是在部件更新队列的循环中一直循环,这个问题就是上面所说的问题,调整了部件更新队列的处理方式就能解决问题。

对按钮部件使用透明效果时,发现不起作用,于是我就用gdb的watch命令查看按钮部件的图形数据结构体中的一个变量,这个变量表示整个图形的全局透明度,经过变量跟踪,发现是实现图形裁剪、图形缩放的函数有问题,这两个函数都修改了输出的图形数据的全局透明度为255,(不透明)。

这个是0.12版的程序截图:

lcpv_1lcpv_2

用到了多线程,也就是调用pthread库提供的函数实现的,但是,在程序退出时发现了一个问题:在退出LCUI后,创建的线程还在运行,而这些线程还在使用之前LCUI提供的资源,由于LCUI已经退出,之前分配的资源已经无效,这就导致了段错误。

为解决这个问题,暂时添加一个函数,实现标题栏中关闭按钮的事件关联,在点击标题栏中的关闭按钮后,先撤销线程,之后再释放LCUI占用的内存资源。

考虑过为LCUI添加线程管理功能,使用LCUI提供的函数创建线程,在这个LCUI程序退出时,LCUI会将这个程序创建的所有线程撤销,然后再释放LCUI分配给程序的资源占用的内存空间,但是,仅仅是考虑罢了,看看以后能否用到。