当前嵌入式系统中GUI 的实现方式主要有两种:一是采用现有的GUI 库;第二种是开发商基于嵌入式操作系统设计特有的GUI 系统。采用第1种方式一般要对通用GUI 库进行剪裁和个性化定制,也往往要支出额外的成本来获得软件授权。相对而言,第2 种方法实现的GUI 占用资源较小、容易满足嵌入式系统的实时性和个性化需求。
本文采用第2 种方式,在嵌入式Linux下使用C 语言实现了一个界面美观、轻量级、占用资源少、执行效率高的图形用户界面系统SKY-GUI.本文的结构如下:第1 部分介绍SKY-GUI 的基本结构;第2部分给出具体的模块设计;第3 部分给出其实验和测试结果;最后总结。
1 SKY-GUI 基本结构
SKY-GUI 系统的功能主要有以下四点:
(1)接收各种输入设备的输入。
(2)建立消息循环,将设备的输入翻译为抽象的事件。
(3)建立窗口和控件对象系统,组织好各个抽象模块的关系,处理各种GUI 事件。
(4)将GUI 对象通过具体形状显示在屏幕上,通过动画将系统的状态变化呈现给用户。
基于这样的设计目标,SKY-GUI 结构如图1 所示。
图1 SKY-GUI 的整体架构
它主要由输入抽象层、显示抽象层、事件系统和窗口系统四大部分组成。输入抽象层管理所有的输入设备,将用户的操作转化成消息送入事件系统。
显示抽象层操作显示设备,提供给窗口系统绘画、贴图、显示字体接口。事件系统为窗口系统提供消息获取、存储和处理的机制。窗口系统是SKY-GUI 的核心,定义了各种控件和窗口,描述它们之间的逻辑关系和消息循环关系。下面将分别对这四大组成部分的设计进行介绍。
2 SKY-GUI 模块设计
2. 1 输入抽象层
输入抽象层为各种输入设备对事件系统的输入接口,它是一个单独的线程,其流程如图2 所示。
图2 输入抽象层流程。
输入抽象层首先对GUI 所需的各种输入设备初始化,而后等待各个设备的输入。当接到设备输入,就把用户对设备的操作翻译成消息,送至事件系统最底层消息队列(在异2. 3. 2 详细讨论)中。
设备输入的翻译过程根据具体的输入设备而定。对于键盘,只要将其键值和该键的状态封成消息。对于鼠标,除了要记录其按键状态,还要根据鼠标当前的位置和屏幕的大小将输入的位移分量转化成鼠标的新位置封入消息。
2. 2 显示抽象层
显示抽象层的作用是为窗口系统提供显示接口函数,包括基本图形接口(画点、画线、填充矩形、区域拷贝、Alpha 混合等)、贴图接口和字体接口三大功能,其结构如图3 所示。
图3 显示抽象层的结构。
显示抽象层在嵌入式Linux 下的基础设备为帧缓冲,对其按坐标写入或读出颜色值即可实现基本的图形接口的功能。
简单的贴图功能用基本图形接口加bmp 格式的文件(图片不经过压缩,其颜色分量按坐标顺序存储)就可以实现。为了让界面更加美观,SKY-GUI移植了开源的jpeg 库和png 库来解压相应格式的压缩图片文件,实现了对这两种图片格式的支持。
对字体的支持当然必不可少。点阵字体把字体的位图按12伊12、16伊16 等格式存入二进制文件,可以比较容易地实现字体接口,但字体不能随意放大缩小,且放大后字体有明显的锯齿。矢量字体用数学方程加字形上的关键点来描述字体,可以进行无级缩放,为界面的绘制带来极大的灵活性。SKY-GUI 移植了开源的Freetype 库,用其寻址矢量字体文件并生成字体位图,实现了对矢量字体的支持。
2. 3 事件系统
事件系统为SKY-GUI 的其他三大部分提供消息发送、存储、获取和处理的功能。其核心为消息、消息队列和消息处理函数。
2. 3. 1 消息定义
SKY-GUI 的消息定义为:
typedef STruct __MSG {
HWND hWnd;摇/ / 窗口指针
int event;摇/ / 事件编号
void* lParam;摇/ / 事件附加参数2
} MSG;
hWnd 为指向窗口的指针,表明此消息需要发给哪个窗口。event 为事件编号,用不同的整数代表不同的事件。wParam 和lParam 为事件的附加参数,它们的含义根据事件类型的不同而定,例如,在鼠标消息中这两个参数就代表光标在屏幕上的坐标位置。
2. 3. 2 消息队列
消息队列是事件系统中的消息的暂存处,它由一个环形先入先出结构的消息数组和一个消息链表组成。消息数组的空间是固定的,一旦被写满,后来的消息只好被丢弃;而消息链表则可以动态扩充大小。在SKY-GUI 中,消息数组主要用来存放底层输入设备的事件(如鼠标、键盘、时钟等等),而消息链表主要用来存放优先级更高且不可丢弃的上层事件(窗口事件和显示事件)。
2. 3. 3 消息操作接口
SKY-GUI 定义了三类消息操作接口:消息发送函数、消息获取函数和事件处理函数。
消息发送函数为输入抽象层和窗口系统提供消息发送接口,包括Post_Msg 函数和Send_Msg 函数,其作用都是向消息队列发送消息,不同之处在于Post_Msg 发送的消息存入消息队列的数组之中,而Send_Msg 发送的消息则存入链表之中。
消息获取函数为Get_Msg 函数,它为窗口提供取得消息的接口。拥有独立线程的窗口( 异2. 4 会描述其结构) 调用它从消息队列中取得一个消息,其中存在链表中的消息更为重要,优先取出。
事件处理函数是窗口处理消息事件的函数接口,在SKY-GUI 中,拥有独立线程的窗口调用Dispatch_Msg 函数来实现对自己消息处理函数的调用。
2. 3. 4 消息处理函数
Dispatch_Msg 只是事件处理的调用接口,窗口收到消息后所采取的具体措施是由消息处理函数决定的,其定义为:
int WndProc ( HWND hwnd, int event, void *wParam,void* lParam);
每一个窗口都有一个函数指针指向自己的消息处理函数,其功能根据不同的窗口有所不同,但总体结构是一样的,如图4 所示。
图4 消息处理函数的结构
其本质上是一个消息处理的分类列表。当窗口调用消息处理函数时,其根据消息类型的不同分别调用底层输入消息、控件消息或显示消息的处理函数,而后再根据具体的消息事件调用相应的处理函数,实现对各种事件的响应。
2. 4 窗口系统
窗口系统为SKY-GUI 系统的核心,它维护了一个完整的窗口列表,定义了窗口系统和事件系统之间的关系,并制定了窗口之间的消息传递机制。
2. 4. 1 窗口的定义
SKY-GUI 中,窗口既包含桌面、对话框这种狭义的窗口,也包含窗口控件( 如按钮、下拉菜单、编辑框等等)这样的广义窗口,其定义为:
typedef struct __WINDOW {
STR32 caption; / / 窗口的名称
RECT rect; / / 窗口的大小、位置
int style; / / 窗口的类型
MsgQueue* pMsgQ; / / 附属于窗口的消息队列
struct __WINDOW*pFocus; / / 活动窗口指针
struct __WINDOW*pParent; / / 父窗口指针
struct __WINDOW*pChldHead; / / 子窗口列表
struct __WINDOW*pNext; / / 兄弟窗口或控件指针
struct __WINDOW*pCtrlHead; / / 控件列表
WNDPROC WndProc; / / 消息处理函数指针
void* data1; / / 窗口私有数据
void* data2; / / 窗口私有数据
void* data3; / / 窗口私有数据
int msg1; / / 窗口状态变化消息
} WINDOW;
caption 为窗口的名称;rect 为保存窗口位置和大小的矩形;style 为窗口的类型;pMsgQ 为窗口的消息队列的指针;pFocus 指向当前窗口的活动子窗口或控件;pParent 指向当前窗口的父窗口;pNext 指向当前窗口的兄弟窗口;pChldHead 用来保存当前窗口的子窗口列表;pCtrlHead 保存当前窗口的控件列表。WndProc 指向当前窗口的消息处理函数;data1、data2、data3 为窗口的私有数据,msg1 为窗口状态变化时需要发出的控件消息,它们的意义根据窗口的类型而定。
从窗口的定义可以看出,本文要实现的是一种树形的窗口关系,整个系统可以拥有一个或多个主窗口,每个主窗口拥有自己的控件和子窗口,而子窗口又可以拥有各自的子窗口和控件,依此类推。
2. 4. 2 窗口与消息队列的关系
窗口定义中含有指向消息队列的指针,但并不是所有的窗口都有自己的消息队列。主窗口(如桌面)需要随时呈现在用户的面前,可以拥有自己的消息队列;其他的子窗口、控件则没有必要拥有自己的消息队列。这两类窗口用不同的方式使用事件系统。
拥有消息队列的主窗口必须拥有自己独立的线程,其消息发送和处理的流程如图5 所示。
图5 拥有消息队列的窗口的消息发送和处理流程。
当其他窗口或输入抽象层需要操作主窗口时,就调用事件系统中的Post_Msg 或Send_Msg 函数向该窗口的消息队列发送一个消息。而主窗口得知有消息输入,就调用事件系统中的Get_Msg 函数取出消息,并使用Dispatch_Msg 调用自己的消息处理函数,找到相应的事件处理方法处理事务。这种消息传递的特点是消息的发送和处理分别在不同的窗口线程中完成,一般用于两个主窗口之间或者输入抽象层和主窗口之间的消息通信。
没有消息队列的子窗口或控件处理消息的流程如图6 所示。
图6 没有消息队列的窗口的消息处理流程
主窗口调用事件系统中的Post_Msg 或Send_Msg 函数向子窗口或控件发送消息,由于该窗口没有自己的消息队列,事件系统不会将该消息保存,而是直接调用该窗口的消息处理函数找到具体的事件处理方式完成这次窗口操作。这种消息传递方式中,发送消息和处理消息都在主窗口的线程中完成,向一个窗口发送消息相当于要求该窗口立刻对事件进行处理。
SKY-GUI 只设置了一个主窗口,即桌面。其他所有的窗口或对话框都作为桌面的子窗口而存在。
这样系统中只有一个窗口线程和一个消息队列,第一种消息处理方式只存在于输入抽象层和桌面之间,而窗口之间的消息处理都采用第二种方式,这样系统的线程开销和消息循环开销会大大减少,从而提高其运行效率。
2. 4. 3 窗口之间的消息传递
窗口之间的消息传递根据消息类型的不同有两种不同方式。
主窗口从消息队列中取得的消息在SKY-GUI中称为底层消息。这类消息是由主窗口处理,还是交给子窗口或是控件处理,是根据窗口定义中的pFocus 变量而定的。当一个窗口的pFocus 不为空时,表示该窗口上方有子窗口被用户使用,消息应该交给它指向的子窗口处理,而这个子窗口也检查自己的pFocus 变量, 依此类推; 只有当一个窗口的pFocus 为空,表示该窗口位于屏幕的最上方,得到的底层消息由窗口自身处理(如图7 左边流程)。
而当控件的状态变化产生控件消息时,其消息处理的过程正好跟上面的流程相反。控件产生的消息首先由自己处理,有必要时再送给pParent 指针指向的父窗口处理,而后还有必要的话再送给父窗口的父窗口处理,最后也可以由主窗口送入消息队列(如图7 右边流程)。
图7 从消息队列读出的消息处理流程(左)和控件产生的消息处理流程(右)。
pFocus 变量和pParent 变量加上这样树形的窗口系统实质上是实现了很多GUI 系统中的窗口的Z序[6](窗口的上下顺序) 功能。该功能是建立在子窗口显示在其父窗口之上,且控件显示在其所属窗口之上的思想上的。由于底层消息一般代表用户对输入设备的操作,所以应该送给位于屏幕最上方的用户正在使用的窗口处理,而控件消息一般代表着GUI 界面自上而下的图形和数据变化过程,所以应该从上到下逐层处理。
3 实验和测试
SKY-GUI 现已嵌入已有的视频监控系统项目中予以使用,它有下拉菜单、输入框、密码框、按钮、软键盘等十几种控件,提供视频监控的操控界面和配置界面。其中的一个典型的界面如图8 所示。
图8 SKY-GUI 在视频监控系统中的典型界面。
为了测试其的性能,本文利用SKY-GUI 和开源的Qt 界面库分别为视频监控系统制作了一套用户界面,其参数对比如表1 所示。
表1 SKY-GUI 与Qt 在视频监控系统中的性能比较。
很显然,Qt 在嵌入式监控系统中占用资源过多,导致其运行速度缓慢并影响到系统的正常编解码。
而SKY-GUI 加上其所需的开源图片和字体库大小也不超过1 M,运行时只占用4 M 左右的内存,这在典型的嵌入式Linux 系统中完全可以接受,也不会影响到监控系统本身的性能。可以看出,Qt 要运用在该监控系统中还需进行更深层次的剪裁和性能优化,而SKY-GUI 则可满足其对界面的功能和性能需求。
4 总结
本文描述了一种嵌入式Linux 平台下GUI 的设计方案。实验证明此设计方案可行,可以满足一般嵌入式平台上的图形界面要求。
评论
查看更多