MiniGUI |
目录
MiniGUI
体系结构
为了帮助更多软件开发人员理解MiniGUI及其编程,同时帮助更多的自由软件开发人员加入MiniGUI的开发,我们将撰写一系列文章介绍MiniGUI的体系结构。本文是系列文章的第一篇,将在整体上对MiniGUI的体系结构作一介绍。其中主要包括:线程的基本概念;基于POSIXThread 的微客户/服务器结构;用来同步微客户/服务器动作的关键数据结构――消息队列;面向对象技术在MiniGUI中的应用等等。最后,文章展望了我们计划在MiniGUI2.0 版开发中采用的体系结构。
概览
到目前为止,MiniGUI的最新发布版本是0.9.96。我们将0.9.xx系列版本定位为MiniGUI1.0 版本的预览版。在0.9.xx版本足够稳定时,我们将发布MiniGUI1.0 版本,同时,目前的代码不会再进行重大调整。在MiniGUI1.0 版本发布之后,我们将立即着手开发MiniGUI2.0 版本。该版本预期将在体系结构上进行重大调整。
引言
1引言
为了吸引更多的自由软件程序员加入MiniGUI2.0 的开发,也为了更好地帮助MiniGUI程序员进行程序开发,我们将撰写一系列的文章介绍MiniGUI1.0 版本的体系结构,重点分析其中的一些缺点以及需要在2.0版本当中进行优化和改造的地方。介绍体系结构的文章计划如下:
·体系结构概览(本文)。将在整体上对MiniGUI1.0 的体系结构进行介绍。重点包括:线程的基本概念;多线程的微客户/服务器体系、多线程通讯的关键数据结构――消息队列;面向对象技术在MiniGUI中的应用等等。
·MiniGUI 的多窗口管理。将介绍MiniGUI的多窗口机制以及相关的窗口类技术。其中涉及到窗口剪切处理和Z序,消息传递,控件类设计和输入法模块设计等等。
·MiniGUI 的图形设备管理。重点介绍MiniGUI是如何处理窗口绘制的。其中主要包括图形上下文的概念,坐标映射,图形上下文的局部、全局和有效剪切域的概念等等。
·图形抽象层和输入抽象层。图形抽象层(GAL)和输入抽象层(IAL)大大提高了MiniGUI的可移植性,并将底层图形设备和上层接口分离开来。这里将重点介绍MiniGUI的GAL和IAL接口,并以EP7211等嵌入式系统为例,说明如何将MiniGUI移植到新的嵌入式平台上。
·多字体和多字符集支持。MiniGUI采用逻辑字体实现多字体和多字符集处理。这一技术成功应用了面向对象技术,通过单一的逻辑接口,可以实现对各种字符集以及各种字体的支持。
POSIX线程
2POSIX 线程
MiniGUI是一个基于线程的窗口系统。为了理解MiniGUI的体系结构,我们有必要首先对线程作一番了解。
2.1什么是线程
线程通常被定义为一个进程中代码的不同执行路线。也就是说,一个进程中,可以有多个不同的代码路线在同时执行。例如,常见的字处理程序中,主线程处理用户输入,而其他并行运行的线程在必要时可在后台保存用户的文档。我们也可以说线程是“轻量级进程”。在Linux中,每个进程由五个基本的部分组成:代码、数据、栈、文件I/O和信号表。因此,系统对进程的处理要花费更多的开支,尤其在进行进程调度和任务切换时。从这个意义上,我们可以将一般的进程理解为重量级进程。在重量级进程之间,如果需要共享信息,一般只能采用管道或者共享内存的方式实现。如果重量级进程通过fork()派生了子进程,则父子进程之间只有代码是共享的。
而我们这里提到的线程,则通过共享一些基本部分而减轻了部分系统开支。通过共享这些基本组成部分,可以大大提高任务切换效率,同时数据的共享也不再困难――因为几乎所有的东西都可以共享。
从实现方式上划分,线程有两种类型:“用户级线程”和“内核级线程”。
用户线程指不需要内核支持而在用户程序中实现的线程,这种线程甚至在象DOS这样的操作系统中也可实现,但线程的调度需要用户程序完成,这有些类似Windows3.x的协作式多任务。另外一种则需要内核的参与,由内核完成线程的调度。这两种模型各有其好处和缺点。用户线程不需要额外的内核开支,但是当一个线程因I/O而处于等待状态时,整个进程就会被调度程序切换为等待状态,其他线程得不到运行的机会;而内核线程则没有各个限制,但却占用了更多的系统开支。
Linux支持内核级的多线程,同时,也可以从Internet上下载一些Linux上的用户级的线程库。Linux的内核线程和其他操作系统的内核实现不同,前者更好一些。大多数操作系统单独定义线程,从而增加了内核和调度程序的复杂性;而Linux则将线程定义为“执行上下文”,它实际只是进程的另外一个执行上下文而已。这样,Linux内核只需区分进程,只需要一个进程/线程数组,而调度程序仍然是进程的调度程序。Linux的clone系统调用可用来建立新的线程。
2.2POSIX 线程
POSIX 标准定义了线程操作的C语言接口。我们可以将POSIX线程的接口划分如下:
·线程的建立和销毁。用来创建线程,取消线程,制造线程取消点等等。
·互斥量操作接口。提供基本的共享对象互斥访问机制。
·信号量操作接口。提供基本的基于信号量的同步机制。不能与SystemV IPC 机制的信号量相混淆。
·条件量操作接口。提供基本的基于条件量的同步机制。尽管信号量和条件量均可以划分为同步机制,但条件量比信号量更为灵活一些,比如可以进行广播,设置等待超时等等。但条件量的操作比较复杂。
·信号操作接口。处理线程间的信号发送和线程信号掩码。
·其他。包括线程局部存储、一次性函数等等。
目前,Linux上兼容POSIX的线程库称为LinuxThreads,它已经作为glibc的一部分而发布。这些函数的名称均以pthread_开头(信号量操作函数以sem_开头)。
为了对线程有一些感性认识,我们在这里举两个例子。
第一个例子在进入main() 函数之后,调用pthread_create函数建立了另一个线程。pthread_create的参数主要有两个,一个是新线程的入口函数(thread_entry),另一个是传递给入口函数的参数(data),而新线程的标识符通过引用参数返回(new_thread)。见清单1。
清单1 新线程的创建
void*thread_entry (void* data)
{
... // do something.
return NULL;
}
intmain (void)
{
pthread_t new_thread;
int data = 2;
pthread_create(&new_thread, NULL, thread_entry, &data);
pthread_join (new_thread, NULL);
}
main() 函数在建立了新线程之后,调用pthread_join函数等待新线程执行结束。pthread_join类似进程级的wait系统调用。当所等待的线程执行结束之后,该函数返回。利用pthread_join可用来实现一些简单的线程同步。注意在上面的例子中,我们忽略了函数调用返回值的错误检查。
第二个例子是利用信号量进行同步的两个线程。这里所使用的例子利用信号量解决了经典的“生产者/消费者”问题(清单2)。我们首先解释信号量的基本概念。
信号量的概念由E.W. Dijkstra 于1965年首次提出。信号量实际是一个整数,进程(也可以是线程)在信号量上的操作分两种,一种称为DOWN,而另外一种称为UP。DOWN操作的结果是让信号量的值减1,UP操作的结果是让信号量的值加1。在进行实际的操作之前,进程首先检查信号量的当前值,如果当前值大于0,则可以执行DOWN操作,否则进程休眠,等待其他进程在该信号量上的UP操作,因为其他进程的UP操作将让信号量的值增加,从而它的DOWN操作可以成功完成。某信号量在经过某个进程的成功操作之后,其他休眠在该信号量上的进程就有可能成功完成自己的操作,这时,系统负责检查休眠进程是否可以完成自己的操作。
为了理解信号量,我们想象某机票定购系统。最初旅客在定票时,一般有足够的票数可以满足定票量。当剩余的机票数为1,而某个旅客现在需要定两张票时,就无法满足该顾客的需求,这时售票小姐让这个旅客留下他的电话号码,如果其他人退票,就可以优先让这个旅客定票。如果最终有人退票,则售票小姐打电话通知上述要定两张票的旅客,这时,该旅客就能够定到自己的票。
我们可以将旅客看成是进程,而定票可看成是信号量上的DOWN操作,退票可看成是信号量上的UP操作,而信号量的初始值为机票总数,售票小姐则相当于操作系统的信号量管理器,由她(操作系统)决定旅客(进程)能不能完成操作,并且在新的条件成熟时,负责通知(唤醒)登记的(休眠的)旅客(进程)。
在操作系统中,信号量的最简单形式是一个整数,多个进程可检查并设置信号量的值。这种检查并设置操作是不可被中断的,也称为“原子”操作。检查并设置操作的结果是信号量的当前值和设置值相加的结果,该设置值可以是正值,也可以是负值。根据检查和设置操作的结果,进行操作的进程可能会进入休眠状态,而当其他进程完成自己的检查并设置操作后,由系统检查前一个休眠进程是否可以在新信号量值的条件下完成相应的检查和设置操作。这样,通过信号量,就可以协调多个进程的操作。
信号量可用来实现所谓的“关键段”。关键段指同一时刻只能有一个进程执行其中代码的代码段。也可用信号量解决经典的“生产者/消费者”问题,“生产者/消费者”问题和上述的定票问题类似。这一问题可以描述如下:
两个进程共享一个公共的、固定大小的缓冲区。其中的一个进程,即生产者,向缓冲区放入信息,另外一个进程,即消费者,从缓冲区中取走信息(该问题也可以一般化为m个生产者和n个消费者)。当生产者向缓冲区放入信息时,如果缓冲区是满的,则生产者进入休眠,而当消费者从缓冲区中拿走信息后,可唤醒生产者;当消费者从缓冲区中取信息时,如果缓冲区为空,则消费者进入休眠,而当生产者向缓冲区写入信息后,可唤醒消费者。
清单2中的例子实际是“生产者/消费者”问题的线程版本。
清单2利用信号量解决“生产者/消费者”问题
/*The classic producer-consumer example, implemented withsemaphores.
All integers between 0 and 9999 should beprinted exactly twice,
once to the right of the arrowand once to the left. */
#include<stdio.h>
#include <pthread.h>
#include<semaphore.h>
#defineBUFFER_SIZE 16
/*Circular buffer of integers. */
structprodcons {
int buffer[BUFFER_SIZE]; /* 实际数据*/
int readpos, writepos; /*读取和写入的位置*/
sem_t sem_read; /* 可读取的元素个数*/
sem_t sem_write; /* 可写入的空位个数*/
};
/*初始化缓冲区*/
voidinit(struct prodcons * b)
{
sem_init(&b->sem_write,0, BUFFER_SIZE - 1);
sem_init(&b->sem_read, 0, 0);
b->readpos = 0;
b->writepos = 0;
}
/*在缓冲区中保存一个整数 */
voidput(struct prodcons * b, int data)
{
/* Wait untilbuffer is not full */
sem_wait(&b->sem_write);
/* Write the data and advance write pointer */
b->buffer[b->writepos] = data;
b->writepos++;
if (b->writepos >= BUFFER_SIZE) b->writepos = 0;
/* Signal that the buffer contains one more element for reading */
sem_post(&b->sem_read);
}
/*从缓冲区读取并删除数据 */
intget(struct prodcons * b)
{
int data;
/* Waituntil buffer is not empty */
sem_wait(&b->sem_read);
/* Read the data and advance read pointer */
data =b->buffer[b->readpos];
b->readpos++;
if(b->readpos >= BUFFER_SIZE) b->readpos = 0;
/*Signal that the buffer has now one more location for writing */
sem_post(&b->sem_write);
return data;
}
/*测试程序:一个线程插入1到10000的整数,另一个线程读取并打印。*/
#defineOVER (-1)
structprodcons buffer;
void* producer(void * data)
{
int n;
for (n = 0; n< 10000; n++) {
printf("%d --->\n",n);
put(&buffer, n);
}
put(&buffer, OVER);
return NULL;
}
void* consumer(void * data)
{
int d;
while (1){
d = get(&buffer);
if (d == OVER) break;
printf("--->%d\n", d);
}
return NULL;
}
intmain(void)
{
pthread_t th_a, th_b;
void *retval;
init(&buffer);
/*建立生产者和消费者线程。*/
pthread_create(&th_a, NULL, producer, 0);
pthread_create(&th_b, NULL, consumer, 0);
/*等待生产者和消费者结束。*/
pthread_join(th_a, &retval);
pthread_join(th_b,&retval);
return 0;
}
在清单 2中,程序首先建立了两个线程分别扮演生产者和消费者的角色。生产者负责将1到1000的整数写入缓冲区,而消费者负责从同一个缓冲区中读取并删除由生产者写入的整数。因为生产者和消费者是两个同时运行的线程,并且要使用同一个缓冲区进行数据交换,因此必须利用一种机制进行同步。清单2中的程序就利用信号量实现了同步。
起初程序初始化了两个信号量(init()函数),分别表示可读取的元素数目(sem_read)和可写入的空位个数(sem_write),并分别初始化为0和缓冲区大小减1。在生产者调用put()函数写入时,它首先对sem_write进行DOWN操作(即sem_wait调用),看是否能够写入,如果此时sem_write信号量的值大于零,则sem_wait可以立即返回,否则生产者将在该sem_write信号量上等待。生产者在将数据写入之后,在sem_read信号量上进行UP操作(即sem_post调用)。此时如果有消费者等待在sem_read信号量上,则可以被系统唤醒而继续运行。消费者线程的操作恰恰相反,该线程调用get()函数时,首先在sem_read上进行DOWN操作,当读取数据并删除之后,在sem_write信号量上进行UP操作。
通过上面的两个例子,读者可以对线程之间的互操作有一个大概了解。如果读者对SystemV IPC机制比较熟悉的话,也可以作一番比较。可以看到,多线程的最大好处是,除堆栈之外,几乎所有的数据均是共享的,因此线程间的通讯效率最高;但最大坏处是,因为共享所有数据,从而非常容易导致线程之间互相破坏数据。
2.3MiniGUI 和多线程
MiniGUI 1.0 版本采用了多线程机制,也就是说,MiniGUI以及运行在MiniGUI之上的所有应用程序均运行在同一个地址空间之内。比起其他基于进程的GUI系统来说,虽然缺少了地址保护,但运行效率却是最高的。
基于PThread的微客户/服务器结构
3基于PThread的微客户/服务器结构
3.1多线程的分层设计
从整体结构上看,MiniGUI是分层设计的,层次结构见图1。在最底层,GAL和IAL提供底层图形接口以及鼠标和键盘的驱动;中间层是MiniGUI的核心层,其中包括了窗口系统必不可少的各个模块;最顶层是API,即编程接口。
GAL和IAL为MiniGUI提供了底层的Linux控制台或者XWindow 上的图形接口以及输入接口,而Pthread是用于提供内核级线程支持的C函数库。
MiniGUI本身运行在多线程模式下,它的许多模块都以单独的线程运行,同时,MiniGUI还利用线程来支持多窗口。从本质上讲,每个线程有一个消息队列,消息队列是实现线程数据交换和同步的关键数据接口。一个线程向消息队列中发送消息,而另一个线程从这个消息队列中获取消息,同一个线程中创建的窗口可共享同一个消息队列。利用消息队列和多线程之间的同步机制,可以实现下面要讲到的微客户/服务器机制。
多线程有其一定的好处,但不方便的是不同的线程共享了同一个地址空间,因此,客户线程可能会破坏系统服务器线程的数据,但有一个重要的优势是,由于共享地址空间,线程之间就没有额外的数据复制开销。
由于MiniGUI是面向嵌入式或实时控制系统的,因此,这种应用环境下的应用程序往往具有单一的功能,从而使得采用多线程而非多进程模式实现图形界面有了一定的实际意义,也更加符合MiniGUI之“mini”的特色。
3.2微客户/服务器结构
在多线程环境中,与多进程间的通讯机制类似,线程之间也有交互和同步的需求。比如,用来管理窗口的线程维持全局的窗口列表,而其他线程不能直接修改这些全局的数据结构,而必须依据“先来先服务”的原则,依次处理每个线程的请求,这就是一般性的客户/服务器模式。MiniGUI利用线程之间的同步操作实现了客户线程和服务器线程之间的微客户/服务器机制,之所以这样命名,是因为客户和服务器是同一进程中的不同线程。
微客户/服务器机制的核心实现主要集中在消息队列数据结构上。比如,MiniGUI中的desktop微服务器管理窗口的创建和销毁。当一个线程要求desktop微服务器建立一个窗口时,该线程首先在desktop的消息队列中放置一条消息,然后进入休眠状态而等待desktop处理这一请求,当desktop处理完成当前任务之后,或正处于休眠状态时,它可以立即处理这一请求,请求处理完成时,desktop将唤醒等待的线程,并返回一个处理结果。
当 MiniGUI在初始化全局数据结构以及各个模块之后,MiniGUI要启动几个重要的微服务器,它们分别完成不同的系统任务:
desktop用于管理MiniGUI窗口中的所有主窗口,包括建立、销毁、显示、隐藏、修改Z-order、获得输入焦点等等。
parsor线程用来从IAL中收集鼠标和键盘事件,并将收集到的事件转换为消息而邮寄给desktop服务器。
timer 线程用来触发定时器事件。该线程启动时首先设置Linux定时器,然后等待desktop线程的结束,即处于休眠状态。当接收到SIGALRM信号时,该线程处理该信号并向desktop服务器发送定时器消息。当desktop接收到定时器消息时,desktop会查看当前窗口的定时器列表,如果某个定时器过期,则会向该定时器所属的窗口发送定时器消息。
多线程通讯的关键数据结构——消息队列
4多线程通讯的关键数据结构--消息队列
4.1消息和消息循环
在任何GUI系统中,均有事件或消息驱动的概念。在MiniGUI中,我们使用消息驱动作为应用程序的创建构架。
在消息驱动的应用程序中,计算机外设发生的事件,例如键盘键的敲击、鼠标键的按击等,都由支持系统收集,将其以事先的约定格式翻译为特定的消息。应用程序一般包含有自己的消息队列,系统将消息发送到应用程序的消息队列中。应用程序可以建立一个循环,在这个循环中读取消息并处理消息,直到特定的消息传来为止。这样的循环称为消息循环。一般地,消息由代表消息的一个整型数和消息的附加参数组成。
应用程序一般要提供一个处理消息的标准函数。在消息循环中,系统可以调用此函数,应用程序在此函数中处理相应的消息。
MiniGUI支持如下几种消息的传递机制。这些机制为多线程环境下的窗口间通讯提供了基本途径:
·通过PostMessage发送。消息发送到消息队列后立即返回。这种发送方式称为“邮寄”消息。如果消息队列中的邮寄消息缓冲区已满,则该函数返回错误值。
·通过PostSyncMessage发送。该函数用来向不同于调用该函数的线程消息队列邮寄消息,并且只有该消息被处理之后,该函数才能返回,因此这种消息称为“同步消息”。
·通过SendMessage发送。该函数可以向任意一个窗口发送消息,消息处理完成之后,该函数返回。如果目标窗口所在线程和调用线程是同一个线程,该函数直接调用窗口过程,如果处于不同的线程,则利用PostSyncMessage函数发送同步消息。
·通过SendNotifyMessage发送。该函数向指定的窗口发送通知消息,将消息放入消息队列后立即返回。由于这种消息和邮寄消息不同,是不允许丢失的,因此,系统以链表的形式处理这种消息。
通过SendAsyncMessage发送。利用该函数发送的消息称为“异步消息”,系统直接调用目标窗口的窗口过程。
读者可以联系我们在第1节中给出的“生产者/消费者”问题而想到一个简单的消息队列的实现,该消息队列可以简单地设计为一个类似清单2的循环队列。但是,GUI系统中的消息队列并不能是一个简单的循环队列,它还要注意到如下一些问题:
消息一般附带有相关的数据,这些数据对各种消息具有不同的含义,在多窗口环境,尤其是多进程环境下,消息数据的有效传递非常重要。
消息作为窗口间进行数据交换的一种方式,要提供多种传递机制。某些情况下,发送消息的窗口要等到这个消息处理完成之后,知道处理的结果之后才能继续执行;而有些情况下,发送消息的窗口只是简单地向接收消息的窗口通知某些事件的发生,一般发送出消息之后就返回。后一种情况类似于邮寄信件,所以通常称为邮寄消息。更有一种较为复杂的情况,就是等待一个可能长时间无法被处理的消息时,发送的消息的窗口设置一个超时值,以便能够在消息得不到及时处理的情况下能够恢复执行。
某些特殊消息的处理也需要注意,比如定时器。当某个定时器的频率很高,而处理这个定时器的窗口的反应速度又很慢,这时如果采用邮寄消息或者发送消息的方式,线性的循环队列最终就会塞满。
最后一个问题是消息优先级的问题。一般情况下,要考虑优先处理鼠标或键盘的输入消息,其次才是重绘和定时器等消息。
特殊消息的处理。由于窗口重绘消息的特殊性(通常比较花费时间),只有当程序将其他消息处理之后,才会处理重绘消息。并且只有存在窗口的无效区域的时候,才会通知程序处理窗口的重绘。
鉴于以上要特殊考虑的问题,MiniGUI中的消息队列要比清单2中的循环队列复杂。参见清单3。
清单3 MiniGUI 的消息队列定义
typedefstruct _MSGQUEUE
{
DWORDdwState; // 消息队列状态
pthread_mutex_tlock; // 互斥锁
sem_t wait; // 等待信号量
PQMSG pFirstNotifyMsg; // 通知消息队列的头
PQMSG pLastNotifyMsg; // 通知消息队列的尾
PSYNCMSGpFirstSyncMsg; // 同步消息队列的头
PSYNCMSG pLastSyncMsg; // 同步消息队列的尾
MSG*msg; // 邮寄消息缓冲区
int len; // 邮寄消息缓冲区长度
int readpos, writepos; //邮寄消息缓冲区的当前读取和写入位置
/*
* One thread can only support eight timers.
* And number of all timers in a MiniGUI applicatoin is 16.
*/
HWND TimerOwner[8]; // 定时器所有者
int TimerID[8]; // 定时器标识符
BYTE TimerMask; // 已使用的定时器掩码
}MSGQUEUE;
typedef MSGQUEUE* PMSGQUEUE;
可以看出,在MiniGUI的消息队列定义中,只有邮寄消息的定义类似清单2中的线性循环队列。上面提到,通知消息类似邮寄消息,但该消息是不允许丢失的,因此,该消息通过链表形式实现。PMSG结构的定义也很简单:
typedefstruct _QMSG
{
MSG Msg;
struct _QMSG* next;
BOOL fromheap;
}QMSG;
typedef QMSG* PQMSG;
用于同步消息传递的数据结构为SYNCMSG,该结构在消息队列中也形成了一个链表,但该结构本身稍微复杂一些:
typedefstruct _SYNCMSG
{
MSG Msg;
int retval;
sem_t sem_handle;
struct _SYNCMSG*pNext;
}SYNCMSG;
typedef SYNCMSG* PSYNCMSG;
可以看到,该结构中有一个信号量,该信号量就是用来通知同步消息的发送线程的。当接收并处理同步消息的线程处理该消息之后,将在retval成员中存放处理结果,然后通过sem_handle信号量唤醒同步消息的发送线程。
在上述消息队列结构的定义中,还有两个分别用来实现互斥访问和同步的成员,即互斥锁lock和信号量wait。互斥锁lock用来实现不同线程对消息队列的互斥访问,比如在获取邮寄消息时的操作如下:
pthread_mutex_lock(&pMsgQueue->lock);
if (pMsgQueue->readpos != pMsgQueue->writepos) {
pMsgQueue->readpos++;
if (pMsgQueue->readpos >= pMsgQueue->len) pMsgQueue->readpos= 0;
pthread_mutex_unlock(&pMsgQueue->lock);
return 1;
}
else
pMsgQueue->dwState &= ~QS_POSTMSG;
pthread_mutex_unlock(&pMsgQueue->lock);
信号量wait用来同步消息循环。一般来说,一个线程在建立窗口之后,要进入消息循环持续地从消息队列中获取消息(通过GetMessage()函数)。当消息队列中没有任何消息时,该线程将进入休眠状态,而当其他线程将消息邮寄或发送到该消息队列之后,将通过信号量wait唤醒该线程:
sem_getvalue(&pMsgQueue->wait, &sem_value);
if(sem_value == 0)
sem_post(&pMsgQueue->wait);
在 MiniGUI的消息队列结构中,第一个成员是消息队列的状态字。该状态字通过标志位表示如下状态:
·消息队列中是否有邮寄消息;
·消息队列中是否有通知消息;
·消息队列中是否有同步消息;
·消息队列中是否有退出消息;
·消息队列中是否有重绘消息;
·消息队列中是否有定时器消息。
通过这些标志,GetMessage()可判断是否需要检查邮寄消息队列、通知消息链表和同步消息链表等等。同时,利用这些标志还可以处理上面提到的一些特殊消息。这里以定时器为例进行说明。
在 MiniGUI中,一个创建了窗口的线程一般拥有一个消息队列,使用该消息队列所有窗口,包括子窗口在内,一共可以建立8个定时器。这些定时器是否到期,体现在消息队列的状态字上――状态字的最低8位分别用来表示这8个定时器是否到期。消息队列中同时还有三个成员:
HWNDTimerOwner[8]; //定时器所有者
int TimerID[8]; // 定时器标识符
BYTE TimerMask; // 已使用的定时器掩码
其中TimerMask表示当前有效的定时器,每位表示一个定时器;TimerID表示这8个定时器的标识符(整数);而TimerOwner则表示定时器的所有者(窗口句柄)。这种定时器的实现方法类似Linux内核中的信号实现。定时器是否有效以及是否到期均由二进制字节的一个位来表示。当GetMessage检查这些标志时发现有某个定时器到期才会获得一个定时器消息。也就是说,定时器消息是不排队的。这样就解决了排队时可能塞满消息队列的问题。
面向对象技术在MiniGUI中的应用
5面向对象技术在MiniGUI中的应用
5.1控件类和控件
MiniGUI 中的每个控件都属于某种子窗口类,是对应子窗口类的实例。这类似于面向对象技术中类和对象的关系。
每个控件的消息实际都是有该控件所属控件类的回调函数处理的,从而可以让每个属于统一控件类的控件均保持有相同的用户界面和处理行为。
但是,如果我们在调用某个控件类的回调函数之前,首先调用自己定义的某个回调函数的话,我们就可以让该控件重载控件类的某些处理行为,从而让该控件一方面继承控件类的大部分处理行为,另一方面又具有自己的特殊行为。这实际就是面向对象中的继承和派生。比如,一般的编辑框会接收所有的键盘输入,当我们希望自己的编辑框只接收数字时,就可以用这种办法屏蔽非数字的字符输入。
5.2GAL 和IAL
在MiniGUI0.3.xx 的开发中,我们引入了图形和输入抽象层(Graphicsand Input Abstract Layer,GAL和IAL)的概念。抽象层的概念类似Linux内核虚拟文件系统的概念。它定义了一组不依赖于任何特殊硬件的抽象接口,所有顶层的图形操作和输入处理都建立在抽象接口之上。而用于实现这一抽象接口的底层代码称为“图形引擎”或“输入引擎”,类似操作系统中的驱动程序。这实际是一种面向对象的程序结构。利用GAL和IAL,MiniGUI可以在许多图形引擎上运行,比如SVGALib和LibGGI,并且可以非常方便地将MiniGUI移植到其他POSIX系统上,只需要根据我们的抽象层接口实现新的图形引擎即可。目前,我们已经编写了基于SVGALib和LibGGI的图形引擎。利用LibGGI,MiniGUI应用程序可以运行在XWindow 上,将大大方便应用程序的调试。我们目前正在进行MiniGUI私有图形引擎的设计开发。通过MiniGUI的私有图形引擎,我们可以最大程度地针对窗口系统对图形引擎进行优化,最终提高系统的图形性能和效率。
GAL和IAL的结构是一样的,我们这里只拿GAL作为实例说明面向对象技术的运用,参见图4。
系统维护一个已注册图形引擎数组,保存每个图形引擎数据结构的指针。系统利用一个指针保存当前使用的图形引擎。一般而言,系统中至少有两个图形引擎,一个是“哑”图形引擎,不进行任何实际的图形输出;一个是实际要使用的图形引擎,比如LibGGI或者SVGALib。每个图形引擎的数据结构定义了该图形引擎的一些信息,比如标识符、属性等,更重要的是,它实现了GAL所定义的各个接口,包括初始化和终止、图形上下文管理、画点处理函数、画线处理函数、矩形框填充函数、调色板函数等等。
如果在某个实际项目中所使用的图形硬件比较特殊,现有的图形引擎均不支持。这时,我们就可以安照GAL所定义的接口实现自己的图形引擎,并指定MiniGUI使用这种私有的图形引擎即可。这种软件技术实际就是面向对象多态性的具体体现。
利用 GAL和IAL,大大提高了MiniGUI的可移植性,并且使得程序的开发和调试变得更加容易。我们可以在XWindow 上开发和调试自己的MiniGUI程序,通过重新编译就可以让MiniGUI应用程序运行在特殊的嵌入式硬件平台上。
5.3字符集和字体支持
在成功引入GAL和IAL之后,我们又在处理字体和字符集的模块当中引入了逻辑字体的概念。逻辑字体是MiniGUI用来处理文本(包括文本输出和文本分析)的顶层接口。逻辑字体接口将各种不同的字体(比如宋体、黑体和揩体)和字体格式(比如等宽字体、变宽字体等光栅字体和TrueType等矢量字体),以及各种不同字符集(ISO-8859、GB2312、Big5、UNICODE等)综合了起来,从而可以通过统一的接口显示不同字符集的不同字体的文本,并且还可以分析各种字符集文本的组成,比如字符、单词等。在多字体和多字符集的支持中,我们也采用了面向对象的软件技术,使得添加新的字体支持和新的字符集支持非常方便。目前,MiniGUI能够支持各种光栅字体和TrueType、AdobeType 1 等矢量字体,并能够支持GB2312、Big5等多字节字符集,UNICODE的支持正在开发当中。
相对 GAL和IAL而言,MiniGUI中的字符集和字体支持更加复杂,涉及到的内容也较多。前面提到,我们通过逻辑字体这一接口,实现了文字输出和文本分析两个功能。实际这两个功能是相互关联的。在进行文本输出时,尤其在处理多字节字符集,比如GB2312或者Big5时,首先要对文本进行分析,以便判断是否是一个属于该字符集的双字节字符。
未来考虑
6在MiniGUI2.0 中的考虑
尽管MiniGUI采用多线程机制实现了一个小巧、高效的窗口系统,但有很多理由希望MiniGUI能够采用多进程机制实现(尽管多进程机制可能带来通讯上的额外开支):
·良好的地址保护。窗口本身的崩溃不会影响MiniGUI的运行,而目前的多线程机制无法提供地址保护。
·信号处理上的问题。在多线程程序中,所有的多线程共享同一个信号处理方式,包括是否忽略、是否捕获等等。这对某些大型软件是很难接受的。
·多线程程序对程序员要求较高。在编写多线程程序时,通常要考虑到函数的“线程安全”问题,即函数是否是可重入的,因此,我们通常不能使用全局或者静态变量。
鉴于上述需求,我们将在接下来的MiniGUI2.0 开发中,进行一些体系结构上的调整,其中最为重要的就是采用进程机制替代线程机制。
多窗口管理和控件及控件类
本文是MiniGUI体系结构系列文章的第二篇,重点介绍MiniGUI的多窗口机制以及相关的窗口类技术。其中涉及到窗口Z序、窗口剪切、控件类和控件以及输入法模块设计等等。
引言
1引言
在任何一个足够复杂的GUI系统中,处理窗口之间的互相剪切是其首要解决的问题。因为多窗口系统首先要确保一个窗口中的绘制输出不会影响到另外一个窗口。为此,GUI系统一般要利用Z序来管理窗口之间的互相剪切关系。根据窗口在Z序中所处的位置,GUI系统要计算每个窗口受剪切的区域,即剪切域。通常,窗口的剪切域定义为互不相交的矩形集合。GUI系统的底层图形引擎在进行输出时,要根据当前输出的剪切域进行输出的剪切操作。从而保证窗口的绘制输出不会互相影响。因为任何一个窗口的创建、销毁、隐藏、显示均有可能影响其他窗口的剪切域,所以首先要有一个高效的剪切域维护算法。本文将详细描述MiniGUI中的剪切域生成算法。
许多人对控件(或者部件)的概念已经相当熟悉了。控件可以理解为主窗口中的子窗口。这些子窗口的行为和主窗口一样,即能够接收键盘和鼠标等外部输入,也可以在自己的区域内进行输出――只是它们的所有活动被限制在主窗口中。MiniGUI也支持子窗口,并且可以在子窗口中嵌套建立子窗口。我们将MiniGUI中的所有子窗口均称为控件。
在 Windows或XWindow中,系统会预先定义一些控件类,当利用某个控件类创建控件之后,所有属于这个控件类的控件均会具有相同的行为和显示。利用这些技术,可以确保一致的人机操作界面,而对程序员来讲,可以像搭积木一样地组建图形用户界面。MiniGUI使用了控件类和控件的概念,并且可以方便地对已有控件进行重载,使得其有一些特殊效果。比如,需要建立一个只允许输入数字的编辑框时,就可以通过重载已有编辑框而实现,而不需要重新编写一个新的控件类。
在多语种环境中,输入法是一个必不可少的模块。输入法提供了将标准键盘输入翻译为适当语种的文字的能力。MiniGUI中也包含有标准的中文简体输入法,包括全拼、五笔和智能拼音等等。本文最后将介绍MiniGUI中的输入法模块实现。
窗口Z序
2窗口Z序
Z序实际定义了窗口之间的层叠顺序。说起“Z序”这个名称,实际是相对屏幕坐标而言的。一般而言,屏幕上的所有窗口均有一个坐标系,即原点在左上角,X轴水平向右,Y轴垂直向下的坐标系。Z序就是相对于一个假想的Z轴而言的,这个Z轴从屏幕外指向屏幕内。窗口在这个Z轴上的值,就确定了其Z序。Z序值大的窗口,覆盖了Z序值小的窗口。
当然,在程序当中,Z序一般表示为一个链表。越接近于链表头的节点,其Z序值就越大。在MiniGUI中,我们维护了两个Z序。其中一个Z序永远位于另一个Z序之上。这样,就可以创建始终位于其他窗口之上的窗口,比如输入法窗口。如果在建立窗口时,指定了WS_EX_TOPMOST扩展属性,就可以创建这样的主窗口。因为Z序的操作实际就是链表的操作,这里就不再赘述。
窗口剪切算法
3窗口剪切算法
有了窗口Z序,我们就可以计算每个窗口的剪切域。我们把因为窗口Z序而产生的剪切域称为“全局剪切域”,这是相对于窗口自身定义的剪切域而言的,我们把后者称为“局部剪切域”。窗口中的所有输出,首先要受到全局剪切域的影响,其次受到局部剪切域的影响。我们在这里重点讲解窗口的全局剪切域的生成和维护。
3.1全局剪切域的生成和维护
在MiniGUI中,剪切域表示为若干互不相交的矩形之并集,这些矩形称为剪切矩形。最初,屏幕上没有任何窗口时,桌面的剪切域由一个矩形组成,即屏幕矩形;当屏幕上只有一个窗口时,该窗口的剪切域由一个矩形组成,该矩形即为窗口在屏幕上的矩形,而桌面的剪切域却可能是由多个矩形组成的。
读者很容易看出,在只有一个窗口的情况下,形成桌面剪切域的矩形最多只能有四个。
此时,如果有一个新的窗口出现,则新的窗口将同时剪切旧的窗口和桌面(图3。窗口的剪切矩形用空心矩形表示,而桌面的剪切矩形用实心矩形表示)。而这时,桌面和旧窗口的剪切域将多出一些矩形,这些矩形应该是原有剪切域中的每个矩形受到新窗口矩形影响之后生成的剪切矩形。同样,原有剪切域中的每个矩形只能最多只能派生出4个新剪切域,而某些矩形根本不会受到新窗口矩形的影响。
这样,我们可以将某个窗口全局剪切域归纳为原有剪切域中排除(Exclude)某个矩形而生成的:
窗口的全局剪切域初始化为窗口矩形。
当窗口之上有其他窗口覆盖时,则该窗口的全局剪切域为排除新窗口矩形之后的剪切域。
沿Z序迭代第2步,直到最顶层窗口。
清单1中的代码是在显示一个新窗口时,MiniGUI处理被该窗口所覆盖的其他所有窗口的代码。这段代码调用了剪切域维护接口中的SubtractClipRect函数计算新的剪切域。
清单1 显示新窗口时计算被新窗口覆盖的窗口的全局剪切域
//clip all windows under this window.
static voidclip_windows_under_this (ZORDERINFO* zorder, PMAINWIN pWin, RECT*rcWin)
{
PZORDERNODE pNode;//窗口z序节点指针
PGCRINFO pGCRInfo;//控件全局剪切域
pNode= zorder->pTopMost;
while (pNode->hWnd !=(HWND)pWin)
pNode =pNode->pNext;//从z序中找到新窗口
pNode = pNode->pNext;
while(pNode)//遍历新窗口下面的窗口
{
if(((PMAINWIN)(pNode->hWnd))->dwStyle & WS_VISIBLE){
pGCRInfo = ((PMAINWIN)(pNode->hWnd))->pGCRInfo;
pthread_mutex_lock(&pGCRInfo->lock);
SubtractClipRect (&pGCRInfo->crgn, rcWin);
pGCRInfo->age ++;
pthread_mutex_unlock (&pGCRInfo->lock);
}
pNode= pNode->pNext;
}
}
与排除矩形相反的操作是包含(Include)某个矩形到剪切域中。这个操作用于隐藏或者销毁某个窗口时。当一个窗口被隐藏或销毁时,该窗口之下的所有窗口将受到影响,此时,要将被隐藏或销毁窗口的矩形包含到这些受影响窗口的全局剪切域中。为此,MiniGUI的剪切域维护接口中有一个函数专用于该类操作(IncludeClipRect)。为确保剪切域中矩形互不相交,该函数首先计算与每个剪切矩形的相交矩形,然后将自己添加到该剪切域中。
但是,在某些情况下,我们必须重新计算所有窗口的全局剪切域,比如在移动某个窗口时。
3.2剪切矩形的私有堆
显然,在剪切域非常复杂,或者窗口非常多时,需要大量的矩形来表示每个窗口的全局剪切域。而在C程序中,如果频繁使用malloc和free申请和释放每个剪切矩形,将带来许多问题。第一,malloc和free是非常耗时的操作;第二,频繁的malloc和free将导致C程序堆的碎片化,从而可能导致将来的内存分配失败。为了避免频繁使用malloc和free,MiniGUI在初始化时,建立了一个私有的堆。我们可以直接从这个堆中分配剪切矩形,而不需要从进程的全局堆中分配剪切矩形。这个私有堆实际是由一些空闲待用的剪切矩形组成的。每次分配时返回该链表的头节点,而在释放时放进该链表的尾节点。如果该链表为空,则利用malloc从进程的全局堆中分配剪切矩形。清单2说明了这个私有堆的初始化和操作。
清单2 从剪切矩形私有堆中分配和释放剪切矩形
PCLIPRECTGUIAPI ClipRectAlloc(PFREECLIPRECTLIST pList)
{
PCLIPRECT pRect;
#ifndef_LITE_VERSION
pthread_mutex_lock(&pList->lock);
#endif
if(pList->head) {
pRect = pList->head;
pList->head = pRect->next;
}
else {
if(pList->free < pList->size) {
pRect = pList->heap + pList->free;
pRect->fromheap = TRUE;
pList->free ++;
}
else {
pRect = malloc (sizeof(CLIPRECT));
if (pRect == NULL)
fprintf (stderr, "GDI error: alloc clip rectfailure!\n");
else
pRect->fromheap = FALSE;
}
}
#ifndef_LITE_VERSION
pthread_mutex_unlock(&pList->lock);
#endif
returnpRect;
}
voidGUIAPI FreeClipRect(PFREECLIPRECTLIST pList, CLIPRECT*pRect)
{
#ifndef _LITE_VERSION
pthread_mutex_lock (&pList->lock);
#endif
pRect->next= NULL;
if (pList->head) {
pList->tail->next = (PCLIPRECT)pRect;
pList->tail = (PCLIPRECT)pRect;
}
else {
pList->head =pList->tail = (PCLIPRECT)pRect;
}
#ifndef_LITE_VERSION
pthread_mutex_unlock(&pList->lock);
#endif
}
主窗口和控件、控件类
4主窗口和控件、控件类
4.1控件类和控件
如果读者曾经编写过Windows应用程序的话,就应该了解窗口类的概念。在Windows中,程序所建立的每个窗口,都对应着某种窗口类。这一概念和面向对象编程中的类、对象的关系类似。借用面向对象的术语,Windows中的每个窗口实际都是某个窗口类的一个实例。在XWindow 编程中,也有类似的概念,比如我们建立的每一个Widget,实际都是某个Widget类的实例。
这样,如果程序需要建立一个窗口,就首先要确保选择正确的窗口类,因为每个窗口类决定了对应窗口实例的表象和行为。这里的表象指窗口的外观,比如窗口边框宽度,是否有标题栏等等,行为指窗口对用户输入的响应。每一个GUI系统都会预定义一些窗口类,常见的有按钮、列表框、滚动条、编辑框等等。如果程序要建立的窗口很特殊,就需要首先注册一个窗口类,然后建立这个窗口类一个实例。这样就大大提高了代码的可重用性。
在 MiniGUI中,我们认为主窗口通常是一种比较特殊的窗口。因为主窗口代码的可重用性一般很低,如果按照通常的方式为每个主窗口注册一个窗口类的话,则会导致额外不必要的存储空间,所以我们并没有在主窗口提供窗口类支持。但主窗口中的所有子窗口,即控件,均支持窗口类(控件类)的概念。MiniGUI提供了常用的预定义控件类,包括按钮(包括单选钮、复选钮)、静态框、列表框、进度条、滑块、编辑框等等。程序也可以定制自己的控件类,注册后再创建对应的实例。清单3中的代码就创建了一个编辑框,一个按钮。
采用控件类和控件实例的结构,不仅可以提高代码的可重用性,而且还可以方便地对已有控件类进行扩展。比如,在需要建立一个只允许输入数字的编辑框时,就可以通过重载已有编辑框控件类而实现,而不需要重新编写一个新的控件类。在MiniGUI中,这种技术称为子类化或者窗口派生。子类化的方法有三种:
·一种是对已经建立的控件实例进行子类化,子类化的结果是只影响这一个控件实例;
·一种是对某个控件类进行子类化,将影响其后创建的所有该控件类的控件实例;
·最后一种是在某个控件类的基础上新注册一个子类化的控件类,不会影响原有控件类。在Windows中,这种技术又称为超类化。
在 MiniGUI中,控件的子类化实际是通过替换已有的窗口过程实现的。清单4中的代码就通过控件类创建了两个子类化的编辑框,一个只能输入数字,而另一个只能输入字母:
清单4 控件的子类化
#defineIDC_CTRL1 100
#define IDC_CTRL2 110
#define IDC_CTRL3 120
#defineIDC_CTRL4 130
#defineMY_ES_DIGIT_ONLY 0x0001
#defineMY_ES_ALPHA_ONLY 0x0002
static WNDPROCold_edit_proc;
static int RestrictedEditBox (HWND hwnd, intmessage, WPARAM wParam, LPARAM lParam)
{
if(message == MSG_CHAR) {
DWORD my_style = GetWindowAdditionalData (hwnd);
/*确定被屏蔽的按键类型*/
if ((my_style & MY_ES_DIGIT_ONLY) && (wParam < '0' ||wParam > '9'))
return 0;
else if(my_style & MY_ES_ALPHA_ONLY)
if (!((wParam >= 'A' && wParam <= 'Z') || (wParam >='a' && wParam <= 'z')))
/* 收到被屏蔽的按键消息,直接返回*/
return 0;
}
/*由老的窗口过程处理其余消息*/
return (*old_edit_proc) (hwnd, message, wParam, lParam);
}
staticint ControlTestWinProc (HWND hWnd, int message, WPARAM wParam, LPARAMlParam)
{
switch (message) {
case MSG_CREATE:
{
HWND hWnd1, hWnd2, hWnd3;
CreateWindow(CTRL_STATIC, "Digit-only box:", WS_CHILD | WS_VISIBLE |SS_RIGHT, 0,
10, 10, 180, 24, hWnd, 0);
hWnd1 = CreateWindow (CTRL_EDIT, "", WS_CHILD | WS_VISIBLE| WS_BORDER, IDC_CTRL1,
200, 10, 180, 24, hWnd, MY_ES_DIGIT_ONLY);
CreateWindow (CTRL_STATIC, "Alpha-only box:", WS_CHILD |WS_VISIBLE | SS_RIGHT, 0,
10, 40, 180, 24, hWnd, 0);
hWnd2 = CreateWindow (CTRL_EDIT, "", WS_CHILD | WS_BORDER |WS_VISIBLE, IDC_CTRL2,
200, 40, 180, 24, hWnd, MY_ES_ALPHA_ONLY);
CreateWindow (CTRL_STATIC, "Normal edit box:", WS_CHILD |WS_VISIBLE | SS_RIGHT, 0,
10, 70, 180, 24, hWnd, 0);
hWnd3 = CreateWindow (CTRL_EDIT, "", WS_CHILD | WS_BORDER |WS_VISIBLE, IDC_CTRL2,
200, 70, 180, 24, hWnd, MY_ES_ALPHA_ONLY);
CreateWindow("button", "Close", WS_CHILD | BS_PUSHBUTTON |WS_VISIBLE, IDC_CTRL4,
100, 100, 60, 24, hWnd, 0);
/*用自定义的窗口过程替换编辑框的窗口过程,并保存老的窗口过程。*/
old_edit_proc = SetWindowCallbackProc (hWnd1,RestrictedEditBox);
SetWindowCallbackProc (hWnd2, RestrictedEditBox);
break;
}
...
}
returnDefaultMainWinProc (hWnd, message, wParam, lParam);
}
在清单 4中,程序首先定义了一个窗口处理过程,即RestrictedEditBox函数。然后,在利用CreateWindow函数建立控件时,将其中两个编辑框的窗口处理过程通过SetWindowCallbackProc替换成了自己定义的RestrictedEditBox函数,并且将该函数返回的值(即老的控件窗口处理过程地址)保存在了old_edit_box变量中。在建立这些编辑框之后,它们的消息将首先由RestrictedEditBox函数处理,然后在某些情况下才由老的窗口处理过程处理。
限于篇幅,另外两种控件子类化的方法就不在这里讲述。
4.2MiniGUI 中控件类的实现
MiniGUI函数库实际维护了一个当前所有控件类的数据结构,其中包含了控件类名称以及对应的控件类信息。该数据结构实际是一个哈希表,哈希表的每个入口包含由一个指针,该指针指向所有名程以某个字母开头(不分大小写)的控件类信息链表。控件类信息结构定义如下:
#defineMAXLEN_CLASSNAME 15
typedef struct_CTRLCLASSINFO
{
char name [MAXLEN_CLASSNAME + 1];
// 控件类名程
/*
* common properties of this class
*/
DWORD dwStyle; //控件类风格
HCURSOR hCursor; //控件光标
int iBkColor; // 控件的背景颜色
int(*ControlProc)(HWND, int, WPARAM,LPARAM);
// 控件处理过程
DWORDdwAddData; // 附加数据
intnUseCount; // 使用计数,即系统中属于该控件类的控件个数
struct _CTRLCLASSINFO* next;
// 下一个控件类信息结构
}CTRLCLASSINFO;
typedef CTRLCLASSINFO* PCTRLCLASSINFO;
在控件类的数据结构中包含了鼠标、光标、控件类的回调函数地址等等信息。在创建属于该控件类的控件时,这些信息会复制到控件数据结构中。这样,新的控件实例就继承了这种控件类的表象和行为。
该哈希表的哈希函数实际非常简单,它的返回值就是控件类名称首字母的英文字母表顺序值:
staticint HashFunc (char* szClassname)
{
/*判断首字符是否为字母*/
if (!isalpha (szClassName[0])) return ERR_CTRLCLASS_INVNAME;
/*讲所有字符转换为大写*/
while (szClassName[i]) {
szClassName[i] = toupper(szClassName[i]);
i++;
if (i > MAXLEN_CLASSNAME)
return ERR_CTRLCLASS_INVLEN;
}
/*获得哈希值*/
return szClassName[0] - 'A';
}
控件类的注册和注销函数非常简单,这里不再赘述。
4.3MiniGUI 中控件的实现
控件结构相对复杂一些。其中包含了控件在父窗口中的位置信息、控件风格、扩展风格、控件鼠标、图标、控件回调函数地址等等:
typedefstruct _CONTROL
{
/*
* 这些成员和MAINWIN结构一致.
*/
short DataType; // 内部使用的数据类型
short WinType; // 内部使用的窗口类型
intleft, top; //控件在父窗口中的位置
int right, bottom;
intcl, ct; // 控件客户区在父窗口中的位置
int cr, cb;
DWORDdwStyle; //控件风格
DWORD dwExStyle; // 控件扩展风格
intiBkColor; // 背景颜色
HMENU hMenu; // 菜单句柄
HACCEL hAccel; // 加速键表句柄
HCURSOR hCursor; //鼠标光标句柄
HICON hIcon; // 图标句柄
HMENU hSysMenu; //系统菜单句柄
HDC privCDC; // 私有DC句柄
INVRGN InvRgn; // 控件的无效区域
PGCRINFO pGCRInfo; // 控件的全局剪切区域
PZORDERNODE pZOrderNode;
// Z 序节点
// 仅对具有WS_EX_CTRLASMAINWIN扩展风格的控件有效
PCARETINFOpCaretInfo; // 插入符消息
DWORDdwAddData; // 控件附加数据
DWORD dwAddData2; // 控件附加数据
int(*ControlProc) (HWND, int, WPARAM, LPARAM); // 控件消息处理过程
char*spCaption; //控件标题
int id; // 控件标识符,整数
SCROLLBARINFOvscroll; // 垂直滚动条信息
SCROLLBARINFO hscroll; // 水平滚动条信息
PMAINWINpMainWin; // 包含该控件的主窗口
struct_CONTROL* pParent;// 控件的父窗口
/*
* Child windows.
*/
struct _CONTROL* children;
// 控件的第一个子控件
struct _CONTROL* active;
// 当前活动子控件
struct _CONTROL* old_under_pointer;
// 老的鼠标鼠标所在子控件
/*
* 下面这些成员只对控件有效
*/
struct _CONTROL* next; //下一个兄弟控件
struct _CONTROL* prev; // 前一个兄弟控件
PCTRLCLASSINFOpcci; // 指向控件所属控件类结构的指针
}CONTROL;
typedef CONTROL* PCONTROL;
很显然,只要将控件的回调函数地址进行替换,就可以非常方便地对控件进行子类化操作。值得一提的是,主窗口的结构定义和控件数据结构定义基本上是相同的,只是在某些成员上有一些小小的差别。
输入法模块的设计
5输入法模块的设计
输入法提供了将标准键盘输入翻译为适当语种的文字的能力。MiniGUI中也包含有标准的中文简体输入法,包括全拼、五笔和智能拼音等等。MiniGUI的输入法是一个相对独立的模块(称为IME),它实际是一个特殊的主窗口。该主窗口将在启动之后,首先将自己注册为输入法窗口。这样,MiniGUI的desktop就知道首先要将按键信息发送到这个主窗口之中,而不是当前的活动主窗口。当活动主窗口发生变化时,desktop会通知输入法窗口当前的活动窗口。这样,当输入法窗口接收到按键消息并且翻译为适当的字符之后,就可以将其发送到当前的活动窗口。
为了实现desktop和IME窗口之间的交互,MiniGUI为输入法窗口定义了如下消息,当活动窗口发生变化时,MiniGUI会向IME窗口发送这些消息:
·MSG_IME_SETTARGET:发送该消息设置输入法的目标活动窗口;
·MSG_IME_OPEN:发送该消息告诉输入法窗口,当前活动窗口是具有WS_EX_IMECOMPOSE扩展风格的窗口,所以应该显示输入法窗口。
·MSG_IME_CLOSE:发送该消息告诉输入法窗口,当前活动窗口不具有WS_EX_IMECOMPOSE扩展风格,所以应该隐藏输入法窗口。
如果一个窗口要成为输入法窗口,则必须完成如下工作:
·注册成为当前输入法;
·处理MSG_IME_SETTARGE消息,并记录当前活动目标窗口;
·翻译按键并将翻译后的结构通过MSG_CHAR消息发送到当前活动的目标窗口;
·处理MSG_IME_OPEN和MSG_IME_CLOSE消息,在切换到需要输入法的活动窗口时自动显示输入法窗口。
小结
6小结
本文重点讲述了MiniGUI中的窗口剪切处理算法。这是任何一个多窗口系统首先要解决的问题。然后,本文介绍了MiniGUI中控件类和控件的实现。最后介绍了MiniGUI中输入法窗口的设计思路。
附:MiniGUI的最新进展
2001年元月03日,MiniGUI的0.9.98版本发布。该版本包括一个我们专门针对PDA等嵌入式系统设计的MiniGUI版本,该版本称为MiniGUI-Lite。下面是对MiniGUI-Lite简单介绍,将来我们还要撰文详细介绍MiniGUI-Lite。
大家都知道,MiniGUI采用了基于线程的体系结构,并且建立了基于线程的消息传递和窗口管理功能。但是,在许多系统中,这种基于线程的结构并不是很好。这是因为一些众所周知的原因造成的--Linux线程,尽管可以提供最大程度上的数据共享,但却造成了系统体系结构的脆弱。如果某个线程因为非法的数据访问而终止运行,则整个进程都将受到影响。与线程结构相反的是采用传统的UNIXIPC 机制建立窗口系统,即类似XWindow 的客户/服务器体系。这种体系结构有它的先天不足,主要是通常的IPC机制无法提供高效的数据复制,大量的CPU资源用于在各进程之间复制数据。在PDA等设备中,这种CPU资源的浪费将最终导致系统性能的降低以及设备耗电量的增加。
为了解决MiniGUI版本因为线程而引入的一些问题,同时也为了让MiniGUI更加适合于嵌入式系统,我们决定开发一个MiniGUILite 版本。这个版本的开发目的是:
1.保持与原先MiniGUI版本在源代码级98%以上的兼容。2.不再使用LinuxThreads。3.可以同时运行多个基于MiniGUILite 的应用程序,即多个进程,并且提供前后台进程的切换。
显然,要同时满足上述三个目的,如果采用传统的C/S结构对现有MiniGUI进行改造,应该不难实现。但前面提到的传统C/S结构的缺陷却无法避免。经过对PDA等嵌入式系统的分析,我们发现,某些PDA产品具有运行多个任务的能力,但同一时刻在屏幕上进行绘制的程序,一般不会超过两个。因此,只要确保将这两个进程的绘制相互隔离,就不需要采用复杂的C/S结构处理多个进程窗口之间的互相剪切。也就是说,在这种产品中,如果采用基于传统C/S结构的多窗口系统,实际是一种浪费。
有了上述认识,我们对MiniGUI-Lite版本进行了如下简化设计:
1.每个进程维护自己的主窗口Z序,同一进程创建的主窗口之间互相剪切。也就是说,除这个进程只有一个线程,只有一个消息循环之外,它与原有的MiniGUI版本之间没有任何区别。每个进程在进行屏幕绘制时,不需要考虑其他进程。2.建立一个简单的客户/服务器体系,但确保最小化进程间的数据复制功能。因此,在服务器和客户之间传递的数据仅限于输入设备的输入数据,以及客户和服务器之间的某些请求和响应数据。3.有一个服务器进程(mginit),它负责初始化一些输入设备,并且通过UNIXDomain 套接字将输入设备的消息发送到前台的MiniGUILite 客户进程。4.服务器和客户被分别限定在屏幕的某两个不相交矩形内进行绘制,同一时刻,只能有一个客户及服务器进行屏幕绘制。其他客户可继续运行,但屏幕输入被屏蔽。服务器可以利用API接口将某个客户切换到前台。同时,服务器和客户之间采用信号和SystemV 信号量进行同步。5.服务器还采用SystemV IPC 机制提供一些资源的共享,包括位图、图标、鼠标、字体等等,以便减少实际内存的消耗。
现在你可以使用MiniGUI-Lite一次运行不止一个MiniGUI应用程序。我们可以从一个称为“mginit”的程序中启动其他MiniGUI程序。如果因为某种原因客户终止,服务器可以继续运行。在我们的发布版本中,有一个称为mglite-exec的软件包,这个软件包里有一个mginit程序,该程序建立了一个虚拟控制台窗口。我们可以从这个虚拟控制台的命令行启动该软件包中其他的程序,甚至可以通过gdb调试这些程序。
我们可以在MiniGUI-Lite程序中创建多个窗口,但不能启动新的线程建立窗口。这是MiniGUI-Lite区别于MiniGUI原有版本的最大不同。除此之外,其他几乎所有的API都和MiniGUI原有版本是兼容的。因此。从MiniGUI原有版本向MiniGUI-Lite版本的移植是非常简单的。不信,请看mglite-exec包中的程序,其中所有的程序均来自miniguiexec包,而每个源文件的改动不超过5行。
逻辑字体以及多字体和多字符集实现
本文是MiniGUI体系结构系列文章的第三篇,重点介绍MiniGUI的逻辑字体支持,主要内容涉及MiniGUI中以面向对象技术为基础构建的多字体和多字符集支持,并举例说明了如何在MiniGUI中实现对新字符集和新字体的支持。
引言
1引言
我们在介绍MiniGUI体系结构的第一篇文章中提到,MiniGUI采用了面向对象的技术实现了GAL、IAL以及多字体和多字符集的支持。字体和字符集的支持,对任何一个GUI系统来讲都是不可缺少的。不过,各种GUI在实现多字体和多字符集的支持时,采用不同的策略。比如,对多字符集的支持,QT/Embedded采用UNICODE为基础实现,这种方法是目前比较常用的方法,是一种适合于通用系统的解决方案。然而,这种方法带来许多问题,其中最主要就是UNICODE和其他字符集之间的转换码表会大大增加GUI系统的尺寸。这对某些嵌入式系统来讲是不能接受的。
MiniGUI在内部并没有采用UNICODE为基础实现多字符集的支持。MiniGUI的策略是,对某个特定的字符集,在内部使用和该字符集完全一致的内码表示。然后,通过一系列抽象的接口,提供对某个特定字符集文本的一致分析接口。该接口既可以用于对字体模块,也可以用来实现多字节字符串的分析功能。如果要增加对某个字符集的支持,只需要实现该字符集的接口即可。到目前为止,MiniGUI已经实现了ISO8859-x的单字节字符集支持,以及GB2312、BIG5、EUCKR、UJIS等多字节字符集的支持。
和字符集类似,MiniGUI也针对字体定义了一系列抽象接口,如果要增加对某种字体的支持,只需实现该字体类型的接口即可。到目前为止,MiniGUI已经实现了对RBF和VBF字体(这是MiniGUI定义的两种光栅字体格式)、TrueType和AdobeType1 字体等的支持。
在多字体和多字符集的抽象接口之上,MiniGUI通过逻辑字体为应用程序提供了一致的接口。
本文重点介绍MiniGUI的逻辑字体、多字体和多字符集的实现,并以EUCKR(韩文)字符集和AdobeType1 字体为例,说明如何在MiniGUI中实现一种新的字符集支持和新的字体类型支持。
逻辑字体、设备字体以及字符集之间的关系
2逻辑字体、设备字体以及字符集之间的关系
在 MiniGUI中,每个逻辑字体至少由一个单字节的设备字体组成。设备字体是直接与底层字体相关联的数据结构。每个设备字体有一个操作集(即font_ops),其中包含了get_char_width、get_char_bitmap等抽象接口。每个MiniGUI所支持的字体类型,比如等宽光栅字体(RBF)、变宽光栅字体(VBF)、TrueType字体、AdobeType1字体等均对应一组字体操作集。通过这个字体操作集,我们就可以从相应的字体文件中获得某个字符的点阵(对光栅字体而言)或者轮廓(对矢量字体而言)。之后,MiniGUI上层的绘图函数就可以将这些点阵输出到屏幕上,最终就可以看到显示在屏幕上的文字。
在设备字体结构中,还有一个字符集操作集(即charset_ops),其中包含了len_first_char、char_offset、len_first_substr等抽象接口。每个MiniGUI所支持的字符集,比如ISO8859-x、GB2312、BIG5等字符集均对应一组字符集操作集。通过这个字符集操作集,我们就可以对某个多种字符集混合的字符串进行文本分析。比如在“ABC中文”这个字符串中,头三个字符是属于ISO8859的字符,而“中文”是属于GB2312的字符。通过调用这两个字符集操作集中的函数,我们就可以了解该字符串中哪些字符是属于ISO8859的字符,哪些字符是属于GB2312的字符,甚至可以进行更加复杂的分析。比如,MiniGUI中的GetFirstWord函数可以从这种字符串中获得第一个单词。比如“ABCDEF 中文”字符串中的第一个单词是“ABC”,而第二个单词是“DEF”,第三个单词和第四个单词分别是“中”和“文”。该函数的实现如下:
intGUIAPI GetFirstWord (PLOGFONT log_font, const char* mstr, intlen,
WORDINFO* word_info)
{
DEVFONT* sbc_devfont= log_font->sbc_devfont;
DEVFONT*mbc_devfont = log_font->mbc_devfont;
if(mbc_devfont) {
intmbc_pos;
mbc_pos= (*mbc_devfont->charset_ops->pos_first_char) (mstr,len);
if (mbc_pos == 0){
len = (*mbc_devfont->charset_ops->len_first_substr) (mstr,len);
(*mbc_devfont->charset_ops->get_next_word)(mstr, len, word_info);
return word_info->len + word_info->nr_delimiters;
}
else if (mbc_pos >0)
len = mbc_pos;
}
(*sbc_devfont->charset_ops->get_next_word)(mstr, len, word_info);
return word_info->len+ word_info->nr_delimiters;
}
该函数首先判断该逻辑字体是否包含多字节设备字体(mbc_devfont是否为空),如果是,则调用多字节字符集对应的操作函数pos_first_char、len_first_substr、get_next_word等函数获得第一个单词信息,并填充word_info结构。如果该逻辑字体只包含单字节设备字体,则直接调用单字节字符集对应的操作函数get_next_word。一般而言,在GetFirstWord等函数中,我们首先要进行多字节字符集的某些判断,比如pos_first_char返回的是字符串中属于该字符集的第一个字符的位置。如果返回值不为零,表明第一个字符是单字节字符;如果为零,才会调用其他函数进行操作。
有了这样的逻辑字体、设备字体和字符集结构定义,当我们需要新添加一种字符集或者字体支持时,只需按照我们的字体操作集和字符集操作集定义对应的新操作集结构即可,而对上层程序没有任何影响。
MiniGUI中的字符集支持3.1字符集操作集
3MiniGUI 中的字符集支持3.1字符集操作集
在 MiniGUI中,每个特定的字符集由对应的字符集操作集来表示。字符集操作集的定义如下(include/gdi.h。前面的数字表示在该文件中的行数,下同):
250typedef struct _CHARSETOPS
251 {
252 int nr_chars; // 该字符集中字符的个数
253 int bytes_per_char; // 每个字符的平均字节数
254 int bytes_maxlen_char; // 字符的最大字节数
255 const char* name; // 字符集名称
256 char def_char [MAX_LEN_MCHAR]; // 默认字符
257
258 int (*len_first_char) (const unsigned char* mstr, intmstrlen);
259 int (*char_offset) (constunsigned char* mchar);
260
261 int(*nr_chars_in_str) (const unsigned char* mstr, intmstrlen);
262
263 int(*is_this_charset) (const unsigned char* charset);
264
265 int (*len_first_substr) (const unsigned char* mstr, intmstrlen);
266 const unsigned char*(*get_next_word) (const unsigned char* mstr,
267 int strlen, WORDINFO* word_info);
268
269 int (*pos_first_char) (const unsigned char* mstr, intmstrlen);
270
271 #ifndef _LITE_VERSION
272 unsigned short (*conv_to_uc16) (const unsigned char* mchar, intlen);
273 #endif /* !LITE_VERSION */
274 } CHARSETOPS;
其中,前几个字段(nr_chars、bytes_per_char、bytes_maxlen_char、name、def_char等)表示了该字符集的一些基本信息,具体含义参见注释。这里需要对bytes_maxlen_char和def_chat作进一步解释:
bytes_maxlen_char用来表示该字符集中字符的最长字节数。通常情况下,一个字符集中的每个字符的长度一般是定长的,但是也有许多例外,比如在GB18303、UNICODE等字符集中,字符的最长字节数可能超过4字节。
def_char用来表示该字符集中的默认字符。该字段主要和字体配合使用。当某个针对该字符集的字体中缺少一些字符的定义时,就需要用默认字体替代这些缺少的字符。
在上述字符集的操作集定义中,后几个字段定义为函数指针,它们均由逻辑字体接口用来进行文本分析:
·len_first_char返回多字节字符串中第一个属于该字符集的字符的长度。若不属于该字符集,则返回0。
·char_offset返回某个字符在该字符集中的位置。该信息可以由设备字体使用,用来从一个字体文件中获取该字符对应的宽度或点阵。
·nr_chars_in_str 计算字符串中属于该字符集的字符个数并返回。注意,传入的字符串必须均为该字符集字符。
·is_this_charset 判断给定的用来表示字符集的名称是否指该字符集。因为对某种特定的字符集,其名称不一定和name字段所定义的名称匹配。比如,对GB2312字符集,就可能有gb2312-1980.0、GB2312_80等各种不同的名称。该函数可以帮助正确判断一个名称是否指该字符集。
·len_first_substr 返回某个多字节字符串中属于该字符集的子字符串长度。如果第一个字符不属于该字符集,则返回为0。
·get_next_word返回多字节字符串中属于该字符集的字符串中下一个单词的信息。对欧美语言来说,单词之间由空格、标点符号、制表符等相隔;对亚洲语言来说,单词通常定义为字符。
pos_first_char该函数返回多字节字符串中属于该字符集的第一个字符的位置。
·conv_to_uc16 该函数将某个属于该字符集的字符,转换为UNICODE的16位内码。该函数主要用来从TrueType字体中获得字符的轮廓信息。因为TrueType字体使用UNICODE定位字符,所以需要这个函数完成特定字符集内码到UNICODE内码的转换。由于MiniGUI-Lite版本尚不支持TrueType字体,所以该函数在MiniGUI-Lite版本中无需定义。
在src/font/charset.c中,定义了系统支持的所有字符集操作集,并由函数GetCharsetOps返回某个字符集名称对应的字符集操作集(src/font/charset.c):
716static CHARSETOPS* Charsets [] =
717 {
718 &CharsetOps_iso8859_1,
719 &CharsetOps_iso8859_5,
720 #ifdef _GB_SUPPORT
721 &CharsetOps_gb2312,
722 #endif
723 #ifdef_BIG5_SUPPORT
724 &CharsetOps_big5,
725#endif
726 #ifdef _EUCKR_SUPPORT
727 &CharsetOps_euckr,
728 #endif
729 #ifdef_UJIS_SUPPORT
730 &CharsetOps_ujis
731#endif
732 };
733
734 #define NR_CHARSETS (sizeof(Charsets)/sizeof(CHARSETOPS*))
735
736 CHARSETOPS*GetCharsetOps (const char* charset_name)
737 {
738 int i;
739
740 for (i = 0; i <NR_CHARSETS; i++) {
741 if ((*Charsets [i]->is_this_charset) (charset_name) ==0)
742 return Charsets [i];
743 }
744
745 return NULL;
746 }
747
3.2新字符集的实现举例
如果我们需要定义一种新的字符集支持时,只需在该文件中添加相应的操作集函数以及对应的操作集结构定义即可,比如,对EUCKR字符集的支持定义如下(src/font/charset.c):
468#ifdef _EUCKR_SUPPORT
469 /************************* EUCKRSpecific Operations ************************/
470 static inteuckr_len_first_char (const unsigned char* mstr, int len)
471{
472 unsigned char ch1;
473 unsigned char ch2;
474
475 if (len <2) return 0;
476
477 ch1 = mstr[0];
478 if (ch1 == '\0')
479 return 0;
480
481 ch2 = mstr[1];
482 if (ch1 >= 0xA1 && ch1<= 0xFE && ch2 >= 0xA1 && ch2 <=0xFE)
483 return2;
484
485 return 0;
486 }
487
488static int euckr_char_offset (const unsigned char* mchar)
489{
490 if(mchar [0] > 0xAD)
491 return ((mchar [0] - 0xA4) * 94 + mchar [1] - 0xA1 - 0x8E);
492 else
493 return((mchar [0] - 0xA1) * 94 + mchar [1] - 0xA1 - 0x8E);
494 }
495
496static int euckr_is_this_charset (const unsigned char* charset)
497{
498 int i;
499 char name [LEN_FONT_NAME + 1];
500
501 for (i = 0; i < LEN_FONT_NAME + 1; i++) {
502 if (charset [i] == '\0')
503 break;
504 name[i] = toupper (charset [i]);
505 }
506 name [i] = '\0';
507
508 if (strstr(name, "EUCKR") )
509 return 0;
510
511 return 1;
512}
513
514 static int euckr_len_first_substr (const unsignedchar* mstr, int mstrlen)
515 {
516 unsigned char ch1;
517 unsigned charch2;
518 int i, left;
519 int sub_len = 0;
520
521 left =mstrlen;
522 for (i = 0; i < mstrlen; i+= 2) {
523 if(left < 2) return sub_len;
524
525 ch1 = mstr [i];
526 if (ch1 == '\0') return sub_len;
527
528 ch2 = mstr [i + 1];
529 if (ch1 >= 0xA1 && ch1 <= 0xFE && ch2 >=0xA1 && ch2 <= 0xFE)
530 sub_len += 2;
531 else
532 return sub_len;
533
534 left -= 2;
535 }
536
537 return sub_len;
538 }
539
540 static inteuckr_pos_first_char (const unsigned char* mstr, int mstrlen)
541{
542 unsigned char ch1;
543 unsigned char ch2;
544 int i,left;
545
546 i = 0;
547 left = mstrlen;
548 while (left){
549 if (left <2) return -1;
550
551 ch1 = mstr [i];
552 if (ch1 == '\0') return -1;
553
554 ch2 = mstr [i + 1];
555 if (ch1 >= 0xA1 && ch1 <= 0xFE && ch2 >=0xA1 && ch2 <= 0xFE)
556 return i;
557
558 i += 1;
559 left-= 1;
560 }
561
562 return -1;
563 }
564
565 #ifndef _LITE_VERSION
566 staticunsigned short euckr_conv_to_uc16 (const unsigned char* mchar, intlen)
567 {
568 return '?';
569 }
570#endif
571
572 static CHARSETOPS CharsetOps_euckr = {
573 8836,
574 2,
575 2,
576 FONT_CHARSET_EUCKR,
577 {'\xA1', '\xA1'},
578 euckr_len_first_char,
579 euckr_char_offset,
580 db_nr_chars_in_str,
581 euckr_is_this_charset,
582 euckr_len_first_substr,
583 db_get_next_word,
584 euckr_pos_first_char,
585 #ifndef _LITE_VERSION
586 euckr_conv_to_uc16
587 #endif
588 };
589/************************* End of EUCKR*************************************/
590 #endif /*_EUCKR_SUPPORT */
MiniGUI中的字体支持
4MiniGUI 中的字体支持
4.1设备字体
在MiniGUI中,设备字体定义如下(include/gdi.h):
319struct _DEVFONT
320 {
321 char name [LEN_DEVFONT_NAME + 1];
322 DWORD style;
323 FONTOPS* font_ops;
324 CHARSETOPS* charset_ops;
325 struct _DEVFONT*sbc_next;
326 struct _DEVFONT*mbc_next;
327 void* data;
328 };
其中各字段说明如下:
name:该设备字体的名称。MiniGUI中设备字体的名称格式如下:
<type>-<name>-<style>-<width>-<height>-<charset1[,charset2]>
其中每个域的含义如下:
type:字体类型,比如RBF(MiniGUI定义的等宽字体格式)、VBF(MiniGUI定义的变宽字体格式)、TTF(TrueType字体)等等。
name:名称,比如Song、Hei、Times等等。
style:该字体的样式,比如黑体、斜体等等。
width:该字体的宽度,对矢量字体来说,可取0。
height:该字体的高度,对矢量字体来说,可取0。
charset1,charset2:该字体适用的字符集名称。
style:字体样式。
font_ops:设备字体对应的字体操作集。
charset_ops:设备字体对应的字符集操作集。
sbc_next、mbc_next:内部使用的链表维护字段。
data:该设备字体相关的内部数据。
在MiniGUI启动时,将根据MiniGUI.cfg文件中的定义建立两个设备字体链表,分别为单字节设备字体链和多字节设备字体链。这两个链表将由CreateLogFont使用,通过查找和匹配,建立对应的逻辑字体。
4.2逻辑字体
逻辑字体的定义如下(include/gdi.h):
228typedef struct _LOGFONT {
229 char type[LEN_FONT_NAME + 1];
230 char family[LEN_FONT_NAME + 1];
231 char charset[LEN_FONT_NAME + 1];
232 DWORDstyle;
233 int size;
234 int rotation;
235 DEVFONT*sbc_devfont;
236 DEVFONT* mbc_devfont;
237} LOGFONT;
238 typedef LOGFONT* PLOGFONT;
显然,每个逻辑字体由最匹配该字体要求(大小、字符集、样式等)的两个设备字体(sbc_devfont和mbc_devfong)组成,分别用来处理多字节字符串中的单字节字符和多字节字符。其中单字节设备字体是必不可少的。
逻辑字体的匹配算法可参见src/gdi/logfont.c和src/font/devfont.c文件。限于篇幅,不再赘述。
4.3设备字体操作集
和字符集操作集一样,MiniGUI中的设备字体操作集针对每种设备字体类型而定义,包括对这种设备字体的各种操作函数(include/gdi.h):
276typedef struct _FONTOPS
277 {
278 int(*get_char_width) (LOGFONT* logfont, DEVFONT*devfont,
279 const unsigned char* mchar, int len);
280 int (*get_str_width) (LOGFONT* logfont, DEVFONT*devfont,
281 const unsigned char* mstr, int n, int cExtra);
282 int (*get_ave_width) (LOGFONT* logfont, DEVFONT* devfont);
283 int (*get_max_width) (LOGFONT* logfont, DEVFONT* devfont);
284 int (*get_font_height) (LOGFONT* logfont, DEVFONT* devfont);
285 int (*get_font_size) (LOGFONT* logfont, DEVFONT* devfont, intexpect);
286 int (*get_font_ascent)(LOGFONT* logfont, DEVFONT* devfont);
287 int (*get_font_descent) (LOGFONT* logfont, DEVFONT* devfont);
288
289/* TODO */
290 // int (*get_font_ABC) (LOGFONT*logfont);
291
292 size_t(*char_bitmap_size) (LOGFONT* logfont, DEVFONT*devfont,
293 const unsigned char* mchar, int len);
294 size_t (*max_bitmap_size) (LOGFONT* logfont, DEVFONT*devfont);
295 const void*(*get_char_bitmap) (LOGFONT* logfont, DEVFONT*devfont,
296 const unsigned char* mchar, int len);
297
298 const void* (*get_char_pixmap) (LOGFONT* logfont, DEVFONT*devfont,
299 const unsigned char* mchar, int len, int* pitch);
300 /* Can be NULL */
301
302 void(*start_str_output) (LOGFONT* logfont, DEVFONT*devfont);
303 /* Can be NULL */
304 int (*get_char_bbox)(LOGFONT* logfont, DEVFONT* devfont,
305 const unsigned char* mchar, int len,
306 int* px, int* py, int* pwidth, int* pheight);
307 /* Can be NULL */
308 void(*get_char_advance) (LOGFONT* logfont, DEVFONT*devfont,
309 int* px, int* py);
310 /* Can be NULL */
311
312 DEVFONT*(*new_instance) (LOGFONT* logfont, DEVFONT* devfont,
313 BOOL need_sbc_font);
314 /* Can be NULL */
315 void(*delete_instance) (DEVFONT* devfont);
316 /* Can be NULL */
317 } FONTOPS;
比如,get_char_width用来获得某个字符的宽度,而get_char_bitmap用来获得某个字符的位图信息等等。
在src/font/rawbitmap.c和src/font/varbitmap.c文件中分别定义了对RBF和VBF两种字体的操作函数,比如对变宽光栅字体来讲(VBF),其get_char_bitmap定义如下(src/font/rawbitmap.c):
155static const void* get_char_bitmap (LOGFONT* logfont, DEVFONT*devfont,
156 const unsigned char* mchar, int len)
157 {
158 int offset;
159 unsigned char eff_char =*mchar;
160 VBFINFO* vbf_info =VARFONT_INFO_P (devfont);
161
162 if(*mchar < vbf_info->first_char || *mchar >vbf_info->last_char)
163 eff_char = vbf_info->def_char;
164
165 if (vbf_info->offset == NULL)
166 offset = (((size_t)vbf_info->max_width + 7) >> 3) *vbf_info->height
167 * (eff_char - vbf_info->first_char);
168 else
169 offset =vbf_info->offset [eff_char - vbf_info->first_char];
170
171 return vbf_info->bits + offset;
172 }
其中,VARFONT_INFO_P是一个宏,用来从设备字体的data字段中获得VBFINFO结构的指针。有了这个指针之后,该函数计算字符位图的偏移量最后返回字符的位图。
4.4新设备字体的实现举例
这里以AdobeType1 字体的实现为例,说明如何在MiniGUI中实现一种新的设备字体。MiniGUI借用了T1Lib函数库实现了对Type1字体的支持。
4.4.1Type1 字体简介
Type1矢量字体1格式由Adobe公司设计,并被该公司的ps标准支持。因此,它在Linux下也被支持得很好。它被X和ghostscript支持。一个典型的Type1字体包括一个afm(adobefont metric) 度量文件,一个外形文件,通常是一个pfb( printer font binary) 或者pfa(printer font ascii) 文件,外形文件包括所有的轮廓,而度量文件包含了所有的度量。比如紧排,连字等信息。
4.4.2T1Lib 简介
T1Lib是用C语言实现的一个库,它可以从AdobeType 1 字体生成位图。它可以使用X11R5或者更新版本提供的光栅化工具的很多功能,但避免了其已知的缺点。当然,T1Lib完全可以在没有X11的环境下工作。T1Lib可以被编译成静态或者动态库,从而可以方便地连接。
这里是T1Lib的一些特性:
·字体通过运行时读取字库而被T1lib得知。即它是灵活可配置的。当然,它只支持Type1字体。
字符或字符串只在需要时才被光栅化。
·对字符串光栅化时支持字符间紧排,并且可以利用一个AFM文件提供紧排信息,如果没有这个文件,T1Lib可以直接生成这些信息,也可以将其输出到一个文件以备后用。
·支持连字,连字是一个好的字体模型会提供的功能,目前,只有TEX和与其相关的软件包对连字支持得比较好。连字信息也包含在AFM文件里。
·支持旋转和各种仿射变换。支持字体扩展,倾斜。
·可以动态载入新的解码矢量。用新的解码矢量解析字体。
·支持5灰度的低分辨率和17灰度的高分辨率的反走样。
·字符串可以被添加下划线,上划线或者横线。
4.4.3Adobe Type1 字体支持的实现
在 MiniGUI设备字体定义中,有一个data字段可用来保存设备字体相关的数据结构。对Type1字体来讲,我们使用TYPE1INFO和TYPE1INSTANCEINFO两个数据结构来存储这种设备字体的类信息和实例信息。
1)TYPE1INFO和TYPE1INSTANCEINFO结构
这两个结构的定义如下(src/font/type1.h):
22typedef struct tagTYPE1GLYPHINFO {
23 int font_id;
24 //BBox font_bbox;
25 //int ave_width;
26 BOOL valid;
27 } TYPE1INFO, *PTYPE1INFO;
28
29 typedef structtagTYPE1INSTANCEINFO {
30 PTYPE1INFO type1_info;
31 int rotation;/*intenthdegrees*/
32 T1_TMATRIX * pmatrix;
33 int size;
34 int font_height;
35 int font_ascent;
36 int font_descent;
37
38 int max_width;
39 int ave_width;
40
41 double csUnit2Pixel;
42 /*
43 * last char or string'sinfo
44 * T1_SetChar,T1_SetString, T1_AASetSting, T1_AASetString all return a static
45 * glyph pointer, we save the relatedinfomation here for later use.
46 * */
47 char last_bitmap_char;
48 char last_pixmap_char;
49 char * last_bitmap_str;
50 char * last_pixmap_str;
51 int last_ascent;
52 int last_descent;
53 int last_leftSideBearing;
54 int last_rightSideBearing;
55 int last_advanceX;
56 int last_advanceY;
57 unsignedlong last_bpp;
58 char * last_bits;
59
60 } TYPE1INSTANCEINFO, *PTYPE1INSTANCEINFO;
61
如前面所说,TYPE1INFO和TYPE1INSTANCEINFO数据结构来存储设备字符的类信息和实例信息。初始华时,其实只是注册一个模板,此时利用TYPE1INFO记住其在T1lib中的FontID,这里valid用来说明该设备字体是否初始化完毕。
当用户创建一逻辑字体时,如果用户选择的是Type1字体的某一种,就会调用font_ops的函数new_instance,该函数根据存在于DevFont的data的TYPE1INFO结构中的id,以及用户提供的相关参数,构造一个TYPE1INSTANCEINFO类型的变量,并放入新的设备字体的私有数据data中。从而每个字体实例可以有自己的各种属性。如旋转度。
前面各个字段的意义可以根据名字推测出来,从csUnix2Pixel开始则是为了实现的方便和高效而自己定义的一些变量,后面解释函数实现时将会说明。last*系列函数主要起缓冲的作用。
2)InitType1Fonts 和TermType1Fonts函数
这两个函数负责整个Type1 字体的初始化和终结。
InitType1Fonts的主要任务是:初始化T1lib,根据配置文件提供的信息,将各种字体注册到T1lib,并为每一个字体生成一个DevFont结构,注册到系统中去。该结构中包括的font_ops,是上层对Type1字体各种操作的窗口。
其实主要的处理功能在T1lib中,每次程序向T1lib注册一个字体,T1lib会返回一个FontID,以后利用该ID向T1lib请求关于对应字体的某些服务。
·TermType1Fonts则是注销Type1字体,关闭T1lib。
·InitType1Fonts注册向系统注册了用来处理AbodeType1 字体的字体操作集,定义如下(src/font/type1.c):
780static FONTOPS type1_font_ops = {
781 get_char_width,
782 get_str_width,
783 get_ave_width,
784 get_max_width,
785 get_font_height,
786 get_font_size,
787 get_font_ascent,
788 get_font_descent,
789 char_bitmap_size,
790 max_bitmap_size,
791 get_char_bitmap,
792 get_char_pixmap,
793 start_str_output,
794 get_char_bbox,
795 get_char_advance,
796 new_instance,
797 delete_instance
798};
先说明一些基本概念。
·ascent:描述某个字符在基准线上有多少扫描线。这里以像素为单位(下同)。
descent:描述某个字符在基准线下有多少扫描线。当字符的底线在基准线之下时,用负值来表示,所以整个字符的高度就是ascent- descent。
·leftSideBearing:某个字符从其原点到最左边像素点的水平距离,也可以称为该字符的leftmargin。
·rightSideBearing:某个字符从其原点到最右边像素点的水平距离,也可以称为该字符的rightmargin。
·advanceX:在某字符的图象被放置后,当前原点需要前进的水平距离。它通常比字符图像的宽度要大,因为两个字符之间存在一定的空白。由于该值对齐至像素,所以一些要求精确的内部计算不能用它,会累积误差。
·advanceY:在某字符的图象被放置后,当前原点需要前进的竖直距离。
这样,get_char_width、get_str_width、get_ave_width、get_max_width、get_font_height、get_font_size、get_font_ascent、get_font_descent、char_bitmap_size、max_bitmap_size、get_char_advance等函数的功能就很明显了,它们其实就是取出字体的一些度量(Metrics)。其实,这些信息都是从T1lib内部取得,需要注意的是T1lib内部使用PS单位,而MiniGUI使用的单位是pixel,需要转换。以下以 get_char_bitmap和get_char_pixmap等函数为例说明。
3)get_char_bitmap 和get_char_pixmap
这两个函数是主要的光栅化函数。它们首先判断一下需要光栅化的字符是否刚刚被光栅化过,如果是,直接返回缓冲里的值。
前面讲过,T1Lib支持5灰度的低分辨率和17灰度的高分辨率的反走样。这里的get_char_bitmap返回普通的光栅化位图,而get_char_pixmap返回经过反走样后的像素位图。如果字体在初始化时调用
T1_AASetLevel(T1_AA_LOW)
则这里使用5灰度像素,如果初始化时是调用:
T1_AASetLevel(T1_AA_HIGH)
则这里使用17灰度像素。
这里使用的反走样其实很简单,就是先将字体放大,然后再取样缩小。低精度是放大四倍(2*2),高精度则是放大16倍(4*4),灰度值则有n+1种。
当然,为了提高性能,每次光栅化的结果都要被放到缓冲里,下次如果要光栅化相同的字符,并且方式相同,则可以大大地提高效率。
4)start_str_output
开始字符串输出时调用该函数。完成一些初始化工作。
5)get_char_bbox
给出当前原点值(*px,*py),调用该函数要求得到在字符被画出后的原点值(新的*px,*py),以及当前字符的宽度和高度。
6)new_instance 和delete_instance
当用户创建一个新的逻辑字体时调用new_instance,当用户删除一个逻辑字体时会调用delete_instance。
new_instance根据传给它的一些参数(size,rotation,font_id等)初始化一个TYPE1INSTANCEINFO类型的变量,并将其与新的设备字体关联,将该设备字体返回。以后上层就通过该设备字体得到字体实例相关的信息。
delete_instance则用来删除相关的数据结构。
小结
5小结
面向对象技术在软件设计当中占有非常重要的地位,但面向对象并不是C++等语言的专利。实际上,在诸如操作系统等系统软件当中,面向对象技术的使用是非常广泛的。利用C语言实现面向对象技术,不仅结构清晰,而且在执行效率等方面也有C++等语言无法相比的优势。从本文描述的字体和字符集的实现当中我们可以看到,采用面向对象技术,将大大提高系统的灵活性和可扩展性。
MiniGUI作为一个面向实时嵌入式系统的图形用户界面支持系统,对其执行效率、可定制、可扩展等方面有非常高的要求。为了提高系统的灵活性和可扩展性,我们在一些关键模块当中使用了面向对象的技术。实践表明,面向对象的技术在MiniGUI中的运用是成功的。
图形抽象层和输入抽象层及Native的实现
本文是MiniGUI体系结构系列文章的第四篇。图形抽象层(GAL)和输入抽象层(IAL)大大提高了MiniGUI的可移植性,并将底层图形设备和上层接口分离开来。这里将重点介绍MiniGUI的GAL和IAL接口,并以最新的MiniGUI-Lite版本为例,介绍基于LinuxFrameBuffer 的Native图形引擎的实现,以及特定嵌入式系统上输入引擎的实现。
引言
1引言
在MiniGUI0.3.xx 的开发中,我们引入了图形和输入抽象层(Graphicsand Input Abstract Layer,GAL和IAL)的概念。抽象层的概念类似Linux内核虚拟文件系统的概念。它定义了一组不依赖于任何特殊硬件的抽象接口,所有顶层的图形操作和输入处理都建立在抽象接口之上。而用于实现这一抽象接口的底层代码称为“图形引擎”或“输入引擎”,类似操作系统中的驱动程序。这实际是一种面向对象的程序结构。利用GAL和IAL,MiniGUI可以在许多已有的图形函数库上运行,比如SVGALib和LibGGI。并且可以非常方便地将MiniGUI移植到其他POSIX系统上,只需要根据我们的抽象层接口实现新的图形引擎即可。比如,在基于Linux的系统上,我们可以在LinuxFrameBuffer 驱动程序的基础上建立通用的MiniGUI图形引擎。实际上,包含在MiniGUI1.0.00 版本中的私有图形引擎(NativeEngine)就是建立在FrameBuffer之上的图形引擎。一般而言,基于Linux的嵌入式系统均会提供FrameBuffer支持,这样私有图形引擎可以运行在一般的PC上,也可以运行在特定的嵌入式系统上。
相比图形来讲,将MiniGUI的底层输入与上层相隔显得更为重要。在基于Linux的嵌入式系统中,图形引擎可以通过FrameBuffer而获得,而输入设备的处理却没有统一的接口。在PC上,我们通常使用键盘和鼠标,而在嵌入式系统上,可能只有触摸屏和为数不多的几个键。在这种情况下,提供一个抽象的输入层,就显得格外重要。
本文将介绍MiniGUI的GAL和IAL接口,并介绍私有图形引擎和特定嵌入式系统下的输入引擎实现。
MiniGUI的GAL和IAL定义
2MiniGUI 的GAL和IAL定义
GAL和IAL的结构是类似的,我们以GAL为例说明MiniGUIGAL 和IAL抽象层的结构。
2.1GAL 和图形引擎
参见图1。系统维护一个已注册图形引擎数组,保存每个图形引擎数据结构的指针。系统利用一个指针保存当前使用的图形引擎。一般而言,系统中至少有两个图形引擎,一个是“哑”图形引擎,不进行任何实际的图形输出;一个是实际要使用的图形引擎,比如LibGGI或者SVGALib,或者NativeEngine。每个图形引擎的数据结构定义了该图形引擎的一些信息,比如标识符、属性等,更重要的是,它实现了GAL所定义的各个接口,包括初始化和终止、图形上下文管理、画点处理函数、画线处理函数、矩形框填充函数、调色板函数等等。
如果在某个实际项目中所使用的图形硬件比较特殊,现有的图形引擎均不支持。这时,我们就可以安照GAL所定义的接口实现自己的图形引擎,并指定MiniGUI使用这种私有的图形引擎即可。这种软件技术实际就是面向对象多态性的具体体现。
利用GAL和IAL,大大提高了MiniGUI的可移植性,并且使得程序的开发和调试变得更加容易。我们可以在XWindow 上开发和调试自己的MiniGUI程序,通过重新编译就可以让MiniGUI应用程序运行在特殊的嵌入式硬件平台上。
在代码实现上,MiniGUI通过GFX数据结构来表示图形引擎,见清单1。
清单 1 MiniGUI 中的图形引擎结构(src/include/gal.h)
55typedef struct tagGFX
56 {
57 char* id;
58
59 // Initialization and termination
60 BOOL (*initgfx) (struct tagGFX* gfx);
61 void (*termgfx) (structtagGFX* gfx);
62
63 //Phisical graphics context
64 GAL_GC phygc;
65 int bytes_per_phypixel;
66 int bits_per_phypixel;
67 int width_phygc;
68 int height_phygc;
69 int colors_phygc;
70 BOOL grayscale_screen;
71
72 // GC properties
73 int (*bytesperpixel) (GAL_GC gc);
74 int (*bitsperpixel) (GAL_GC gc);
75 int (*width)(GAL_GC gc);
76 int (*height) (GAL_GC gc);
77 int (*colors) (GAL_GC gc);
78
79 // Allocation and release of graphics context
80 int (*allocategc) (GAL_GC gc, int width, intheight, int depth,
81 GAL_GC* newgc);
82 void (*freegc) (GAL_GC gc);
83 void (*setgc) (GAL_GC gc);
84
85 // Clipping of graphics context
86 void (*enableclipping) (GAL_GC gc);
87 void (*disableclipping)(GAL_GC gc);
88 int (*setclipping) (GAL_GC gc, int x1, int y1, int x2, int y2);
89 int (*getclipping)(GAL_GC gc, int* x1, int* y1, int* x2, int* y2);
90
91 // Background and foreground colors
92 int (*getbgcolor)(GAL_GC gc, gal_pixel* color);
93 int (*setbgcolor) (GAL_GC gc, gal_pixelcolor);
94 int (*getfgcolor) (GAL_GC gc, gal_pixel* color);
95 int (*setfgcolor) (GAL_GC gc, gal_pixelcolor);
96
97 //Convertion between gal_color and gal_pixel
98 gal_pixel (*mapcolor) (GAL_GC gc, gal_color *color);
99 int (*unmappixel) (GAL_GC gc, gal_pixelpixel, gal_color* color);
100 int (*packcolors) (GAL_GC gc, void* buf, gal_color* colors, intlen);
101 int (*unpackpixels) (GAL_GC gc, void* buf, gal_color* colors, intlen);
102
103 // Paletteoperations
104 int (*getpalette) (GAL_GC gc, int s, int len, gal_color* cmap);
105 int (*setpalette) (GAL_GC gc, int s, int len,gal_color* cmap);
106 int (*setcolorfulpalette) (GAL_GC gc);
107
108 // Box operations
109 size_t (*boxsize) (GAL_GC gc, int w, int h);
110 int (*fillbox) (GAL_GC gc, int x, int y, intw, int h,
111 gal_pixel pixel);
112 int (*putbox) (GAL_GC gc, int x, int y, int w, int h, void* buf);
113 int (*getbox) (GAL_GC gc, int x, int y, intw, int h, void* buf);
114 int (*putboxmask) (GAL_GC gc, int x, int y, int w, int h, void* buf,gal_pixel cxx);
115 int (*putboxpart) (GAL_GC gc, int x, int y, int w, int h, intbw,
116 int bh, void* buf, int xo, int yo);
117 int (*putboxwithop) (GAL_GC gc, int x, int y,int w, int h,
118 void* buf, int raster_op);
119 int (*scalebox) (GAL_GC gc, int sw, int sh, void*srcbuf,
120 int dw, int dh, void* dstbuf);
121
122 int (*copybox) (GAL_GC gc, int x, int y, intw, int h, int nx, int ny);
123 int (*crossblit) (GAL_GC src, int sx, int sy, int sw, intsh,
124 GAL_GC dst, int dx, int dy);
125
126 //Horizontal line operaions
127 int (*drawhline) (GAL_GC gc, int x, int y, int w, gal_pixelpixel);
128 int (*puthline) (GAL_GC gc, int x, int y, int w, void*buf);
129 int (*gethline) (GAL_GC gc, int x, int y, int w, void*buf);
130
131 // Vertical lineoperations
132 int (*drawvline) (GAL_GC gc, int x, int y, int h, gal_pixelpixel);
133 int (*putvline) (GAL_GC gc, int x, int y, int h, void*buf);
134 int (*getvline) (GAL_GC gc, int x, int y, int h, void*buf);
135
136 // Pixeloperations
137 int (*drawpixel) (GAL_GC gc, int x, int y, gal_pixel pixel);
138 int (*putpixel) (GAL_GC gc, int x, int y,gal_pixel color);
139 int (*getpixel) (GAL_GC gc, int x, int y, gal_pixel* color);
140
141 // Other drawing
142 int (*circle) (GAL_GC gc, int x, int y, int r, gal_pixel pixel);
143 int (*line) (GAL_GC gc, int x1, int y1, intx2, int y2,
144 gal_pixel pixel);
145 int (*rectangle) (GAL_GC gc, int l, int t, int r, intb,
146 gal_pixel pixel);
147
148 // SimpleCharacter output
149 int (*putchar) (GAL_GC gc, int x, int y, char c);
150 int (*putstr) (GAL_GC gc, int x, int y, constchar* str);
151 int (*getcharsize) (GAL_GC gc, int* width, int* height);
152 int (*setputcharmode) (GAL_GC gc, intbkmode);
153 int (*setfontcolors) (GAL_GC gc,
154 gal_pixel fg, gal_pixel bg);
155
156 //Asynchronous mode support
157 void(*flush) (GAL_GC gc);
158 void(*flushregion) (GAL_GC gc, int x, int y, int w, int h);
159
160 // Panic
161 void (*panic) (intexitcode);
162
163 } GFX;
164
165 extern GFX* cur_gfx;
系统启动之后,将根据配置寻找特定的图形引擎作为当前的图形引擎,并且对全局变量cur_gfx赋值。之后,当MiniGUI需要在屏幕上进行绘制之后,调用当前图形引擎的相应功能函数。比如,在画水平线时如下调用:
(*cur_gfx->drawhline)(gc, x, y, w, pixel);
为方便程序书写,我们还定义了如下C语言宏:
167#define PHYSICALGC (cur_gfx->phygc)
168 #define BYTESPERPHYPIXEL (cur_gfx->bytes_per_phypixel)
169 #defineBITSPERPHYPIXEL (cur_gfx->bits_per_phypixel)
170 #defineWIDTHOFPHYGC (cur_gfx->width_phygc)
171 #define HEIGHTOFPHYGC (cur_gfx->height_phygc)
172 #define COLORSOFPHYGC (cur_gfx->colors_phygc)
173 #define GRAYSCALESCREEN (cur_gfx->grayscale_screen)
174
175 #defineGAL_BytesPerPixel (*cur_gfx->bytesperpixel)
176 #define GAL_BitsPerPixel (*cur_gfx->bitsperpixel)
177 #define GAL_Width (*cur_gfx->width)
178 #define GAL_Height (*cur_gfx->height)
179 #define GAL_Colors (*cur_gfx->colors)
180
181 #define GAL_InitGfx (*cur_gfx->initgfx)
182 #define GAL_TermGfx (*cur_gfx->termgfx)
183
184 #define GAL_AllocateGC (*cur_gfx->allocategc)
185 #define GAL_FreeGC (*cur_gfx->freegc)
186
...
198
199 #defineGAL_MapColor (*cur_gfx->mapcolor)
200 #define GAL_UnmapPixel (*cur_gfx->unmappixel)
201 #define GAL_PackColors (*cur_gfx->packcolors)
202 #define GAL_UnpackPixels (*cur_gfx->unpackpixels)
203
...
208 #defineGAL_BoxSize (*cur_gfx->boxsize)
209 #define GAL_FillBox (*cur_gfx->fillbox)
210 #define GAL_PutBox (*cur_gfx->putbox)
211 #define GAL_GetBox (*cur_gfx->getbox)
212 #define GAL_PutBoxMask (*cur_gfx->putboxmask)
213 #define GAL_PutBoxPart (*cur_gfx->putboxpart)
214 #define GAL_PubBoxWithOp (*cur_gfx->putboxwithop)
215 #define GAL_ScaleBox (*cur_gfx->scalebox)
...
224 #define GAL_DrawVLine (*cur_gfx->drawvline)
225 #define GAL_PutVLine (*cur_gfx->putvline)
226 #define GAL_GetVLine (*cur_gfx->getvline)
这样,上述画线函数可以如下书写:
GAL_DrawVLine(gc, x, y, w, pixel);
显然,只要在系统初始化时能够根据设定对cur_gfx进行适当的赋值,MiniGUI就能够在相应的图形引擎之上进行绘制。
对底层图形引擎的调用,主要集中在MiniGUI的GDI函数中。比如,要绘制一条直线,MiniGUI的LineTo函数定义如清单2所示:
清单 2 LineTo 函数(src/gdi/draw-lite.c)
255void GUIAPI LineTo (HDC hdc, int x, int y)
256 {
257 PCLIPRECT pClipRect;
258 PDC pdc;
259 RECT rcOutput;
260 int startx,starty;
261
262 pdc =dc_HDC2PDC(hdc);
263
264 if(dc_IsGeneralHDC(hdc)) {
265 if (!dc_GenerateECRgn (pdc, FALSE)) {
266 return;
267 }
268 }
269
270 // Transfer logical to device to screen here.
271 startx = pdc->CurPenPos.x;
272 starty =pdc->CurPenPos.y;
273
274 // Movethe current pen pos.
275 pdc->CurPenPos.x= x;
276 pdc->CurPenPos.y =y;
277
278 coor_LP2SP(pdc, &x,&y);
279 coor_LP2SP(pdc, &startx,&starty);
280 rcOutput.left = startx -1;
281 rcOutput.top = starty -1;
282 rcOutput.right = x + 1;
283 rcOutput.bottom = y + 1;
284 NormalizeRect(&rcOutput);
285
286 IntersectRect (&rcOutput, &rcOutput,&pdc->ecrgn.rcBound);
287 if(!dc_IsMemHDC(hdc) ) ShowCursorForGDI(FALSE, &rcOutput);
288
289 // set graphics context.
290 GAL_SetGC(pdc->gc);
291 GAL_SetFgColor (pdc->gc,pdc->pencolor);
292
293 pClipRect =pdc->ecrgn.head;
294 while(pClipRect)
295 {
296 if (DoesIntersect (&rcOutput, &pClipRect->rc)){
297 GAL_SetClipping (pdc->gc, pClipRect->rc.left,pClipRect->rc.top,
298 pClipRect->rc.right - 1, pClipRect->rc.bottom -1);
299
300 if(starty == y) {
301 if (startx > x)
302 GAL_DrawHLine (pdc->gc, x, y, startx - x,pdc->pencolor);
303 else
304 GAL_DrawHLine (pdc->gc, startx, y, x - startx,pdc->pencolor);
305 }
306 else
307 GAL_Line (pdc->gc, startx, starty, x, y,pdc->pencolor);
308 }
309
310 pClipRect = pClipRect->next;
311 }
312
313 if (!dc_IsMemHDC (hdc))ShowCursorForGDI (TRUE, &rcOutput);
314 }
在MiniGUI的所有绘图函数中,要依次做如下几件事:
进行逻辑坐标到设备坐标的转换;
检查是否应该重新生成有效剪切域;
计算输出矩形,并判断是否隐藏鼠标;
由于底层引擎尚不支持剪切域,因此,对剪切域中的每个剪切矩形,调用底层绘图函数输出一次。
在上面的四个步骤当中,第3步和第4步实际可以放到底层引擎当中,从而能够大大提高MiniGUI的绘图效率。不过,这种性能上的提高,对块输出,比如填充矩形、输出位图来讲,并不是非常明显。在将来的底层图形引擎当中,我们将针对上述两点,进行较大的优化以提高图形输出效率。
2.2IAL 和输入引擎
如前所属,MiniGUIIAL 结构和GAL结构类似。在代码实现上,MiniGUI通过INPUT数据结构来表示输入引擎,见清单3。
清单 3 MiniGUI 中的输入引擎结构(src/include/ial.h)
34typedef struct tagINPUT
35 {
36 char* id;
37
38 // Initialization and termination
39 BOOL (*init_input) (struct tagINPUT *input, const char* mdev, constchar* mtype);
40 void (*term_input)(void);
41
42 // Mouseoperations
43 int (*update_mouse) (void);
44 int (*get_mouse_x) (void);
45 int (*get_mouse_y) (void);
46 void(*set_mouse_xy) (int x, int y);
47 int (*get_mouse_button) (void);
48 void (*set_mouse_range) (int minx, int miny,int maxx,int maxy);
49
50 // Keyboard operations
51 int (*update_keyboard) (void);
52 char* (*get_keyboard_state) (void);
53 void(*suspend_keyboard) (void);
54 void (*resume_keyboard) (void);
55 void (*set_leds) (unsigned int leds);
56
57 // Event
58 #ifdef _LITE_VERSION
59 int (*wait_event) (int which, int maxfd, fd_set *in, fd_set *out,fd_set *except,
60 struct timeval *timeout);
61 #else
62 int (*wait_event) (int which, fd_set *in, fd_set *out, fd_set*except,
63 struct timeval *timeout);
64 #endif
65 }INPUT;
66
67 extern INPUT* cur_input;
系统启动之后,将根据配置寻找特定的输入引擎作为当前的输入引擎,并且对全局变量cur_input赋值。
(*cur_gfx->drawhline)(gc, x, y, w, pixel);
为方便程序书写,我们还定义了如下C语言宏:
69#define IAL_InitInput (*cur_input->init_input)
70 #defineIAL_TermInput (*cur_input->term_input)
71 #defineIAL_UpdateMouse (*cur_input->update_mouse)
72 #defineIAL_GetMouseX (*cur_input->get_mouse_x)
73 #defineIAL_GetMouseY (*cur_input->get_mouse_y)
74 #defineIAL_SetMouseXY (*cur_input->set_mouse_xy)
75 #defineIAL_GetMouseButton (*cur_input->get_mouse_button)
76 #defineIAL_SetMouseRange (*cur_input->set_mouse_range)
77
78 #defineIAL_UpdateKeyboard (*cur_input->update_keyboard)
79 #defineIAL_GetKeyboardState (*cur_input->get_keyboard_state)
80 #defineIAL_SuspendKeyboard (*cur_input->suspend_keyboard)
81 #defineIAL_ResumeKeyboard (*cur_input->resume_keyboard)
82 #defineIAL_SetLeds(leds) if(cur_input->set_leds) (*cur_input->set_leds) (leds)
83
84 #define IAL_WaitEvent (*cur_input->wait_event)
在src/kernel/event.c中,我们如下调用底层的输入引擎,从而将输入引擎的数据转换为MiniGUI上层能够理解的消息(以MiniGUI-Lite为例),见清单4:
清单 4 MiniGUI 对底层输入事件的处理
172#ifdef _LITE_VERSION
173 BOOL GetLWEvent (int event, PLWEVENTlwe)
174 {
175 static LWEVENT old_lwe ={0, 0};
176 unsigned int interval;
177 int button;
178 PMOUSEEVENT me =&(lwe->data.me);
179 PKEYEVENT ke =&(lwe->data.ke);
180 unsigned char*keystate;
181 int i;
182 int make; /* 0 = release, 1 =presse */
183
184 if (event == 0){
185 if(timer_counter >= timeout_count) {
186
187 timeout_count = timer_counter + repeat_threshold;
188
189 // repeat last event
190 if (old_lwe.type == LWETYPE_KEY
191 && old_lwe.data.ke.event == KE_KEYDOWN) {
192 memcpy (lwe, &old_lwe, sizeof (LWEVENT));
193 lwe->data.ke.status |= KE_REPEATED;
194 return 1;
195 }
196
197 if (!(old_lwe.type == LWETYPE_MOUSE
198 && (old_lwe.data.me.event == ME_LEFTDOWN||
199 old_lwe.data.me.event == ME_RIGHTDOWN ||
200 old_lwe.data.me.event == ME_MIDDLEDOWN))) {
201 // reset delay time
202 timeout_count = timer_counter + timeout_threshold;
203 }
204
205 // reset delay time
206 lwe->type = LWETYPE_TIMEOUT;
207 lwe->count = timer_counter;
208 return 1;
209 }
210 return0;
211 }
212
213 timeout_count = timer_counter + timeout_threshold;
214 // There was a event occurred.
215 if(event & IAL_MOUSEEVENT) {
216 lwe->type = LWETYPE_MOUSE;
217 if (RefreshCursor(&me->x, &me->y, &button)){
218 me->event = ME_MOVED;
219 time1 = 0;
220 time2 = 0;
221
222 if (oldbutton == button)
223 return 1;
224 }
225
226 if (!(oldbutton & IAL_MOUSE_LEFTBUTTON) &&
227 (button & IAL_MOUSE_LEFTBUTTON) )
228 {
229 if (time1) {
230 interval = timer_counter - time1;
231 if (interval <= dblclicktime)
232 me->event = ME_LEFTDBLCLICK;
233 else
234 me->event = ME_LEFTDOWN;
235 time1 = 0;
236 }
237 else {
238 time1 = timer_counter;
239 me->event = ME_LEFTDOWN;
240 }
241 goto mouseret;
242 }
243
244 if ((oldbutton & IAL_MOUSE_LEFTBUTTON) &&
245 !(button & IAL_MOUSE_LEFTBUTTON) )
246 {
247 me->event = ME_LEFTUP;
248 goto mouseret;
249 }
250
251 if (!(oldbutton & IAL_MOUSE_RIGHTBUTTON) &&
252 (button & IAL_MOUSE_RIGHTBUTTON) )
253 {
254 if (time2) {
255 interval = timer_counter - time2;
256 if (interval <= dblclicktime)
257 me->event = ME_RIGHTDBLCLICK;
258 else
259 me->event = ME_RIGHTDOWN;
260 time2 = 0;
261 }
262 else {
263 time2 = timer_counter;
264 me->event = ME_RIGHTDOWN;
265 }
266 goto mouseret;
267 }
268
269 if ((oldbutton & IAL_MOUSE_RIGHTBUTTON) &&
270 !(button & IAL_MOUSE_RIGHTBUTTON) )
271 {
272 me->event = ME_RIGHTUP;
273 goto mouseret;
274 }
275 }
276
277 if(event & IAL_KEYEVENT) {
278 lwe->type = LWETYPE_KEY;
279 keystate = IAL_GetKeyboardState ();
280 for(i = 0; i < NR_KEYS; i++) {
281 if(!oldkeystate[i] && keystate[i]) {
282 ke->event = KE_KEYDOWN;
283 ke->scancode = i;
284 olddownkey = i;
285 break;
286 }
287 if(oldkeystate[i] && !keystate[i]) {
288 ke->event = KE_KEYUP;
289 ke->scancode = i;
290 break;
291 }
292 }
293 if (i ==NR_KEYS) {
294 ke->event = KE_KEYDOWN;
295 ke->scancode = olddownkey;
296 }
297
298 make= (ke->event == KE_KEYDOWN)?1:0;
299
300 if (i != NR_KEYS) {
301 unsigned leds;
302
303 switch (ke->scancode) {
304 case SCANCODE_CAPSLOCK:
305 if (make && caps_off) {
306 capslock = 1 - capslock;
307 leds = slock | (numlock << 1) | (capslock <<2);
308 IAL_SetLeds (leds);
309 status = (DWORD)leds << 16;
310 }
311 caps_off = 1 - make;
312 break;
313
314 case SCANCODE_NUMLOCK:
315 if (make && num_off) {
316 numlock = 1 - numlock;
317 leds = slock | (numlock << 1) | (capslock <<2);
318 IAL_SetLeds (leds);
319 status = (DWORD)leds << 16;
320 }
321 num_off = 1 - make;
322 break;
323
324 case SCANCODE_SCROLLLOCK:
325 if (make & slock_off) {
326 slock = 1 - slock;
327 leds = slock | (numlock << 1) | (capslock <<2);
328 IAL_SetLeds (leds);
329 status = (DWORD)leds << 16;
330 }
331 slock_off = 1 - make;
332 break;
333
334 case SCANCODE_LEFTCONTROL:
335 control1 = make;
336 break;
337
338 case SCANCODE_RIGHTCONTROL:
339 control2 = make;
340 break;
341
342 case SCANCODE_LEFTSHIFT:
343 shift1 = make;
344 break;
345
346 case SCANCODE_RIGHTSHIFT:
347 shift2 = make;
348 break;
349
350 case SCANCODE_LEFTALT:
351 alt1 = make;
352 break;
353
354 case SCANCODE_RIGHTALT:
355 alt2 = make;
356 break;
357
358 }
359
360 status &= 0xFFFFF0C0;
361
362 status |= (DWORD)((capslock << 8)|
363 (numlock << 7) |
364 (slock << 6) |
365 (control1 << 5) |
366 (control2 << 4) |
367 (alt1 << 3) |
368 (alt2 << 2) |
369 (shift1 << 1) |
370 (shift2));
371
372 // Mouse button status
373 if (oldbutton & IAL_MOUSE_LEFTBUTTON)
374 status |= 0x00000100;
375 else if (oldbutton & IAL_MOUSE_RIGHTBUTTON)
376 status |= 0x00000200;
377 }
378 ke->status= status;
379 SHAREDRES_SHIFTSTATUS = status;
380 memcpy (oldkeystate, keystate, NR_KEYS);
381 memcpy (&old_lwe, lwe, sizeof (LWEVENT));
382 return 1;
383 }
384
385 old_lwe.type = 0;
386 return 0;
387
388mouseret:
389 status &=0xFFFFF0FF;
390 oldbutton =button;
391 if (oldbutton &IAL_MOUSE_LEFTBUTTON)
392 status |= 0x00000100;
393 if (oldbutton &IAL_MOUSE_RIGHTBUTTON)
394 status |= 0x00000200;
395 me->status =status;
396 SHAREDRES_SHIFTSTATUS =status;
397 memcpy (&old_lwe, lwe,sizeof (LWEVENT));
398 return 1;
399}
#endif
从这段代码中可以看出,对定点设备来讲,比如鼠标或者触摸屏,MiniGUI能够自动识别移动信息,也能够自动识别用户的单击和双击事件。这样,底层引擎只需提供位置信息和当前的按键状态信息就可以了。对类似键盘的东西,MiniGUI也能够自动进行重复处理。当一个按键按下一段时间之后,MiniGUI将连续发送该按键的消息给上层处理。对特定的嵌入式系统来讲,可以将某些按键映射为PC的某些键盘键,上层只需处理这些键盘键消息的按下和释放即可。这样,嵌入式系统上的某些键的功能就可以在PC上进行模拟了。
Native图形引擎的实现
3Native 图形引擎的实现
Native图形引擎的图形驱动程序已经提供了基于Linux内核提供FrameBuffer之上的驱动,目前包括对线性2bpp、4bpp、8bpp和16bpp显示模式的支持。前面已经看到,GAL提供的接口函数大多数与图形相关,它们主要就是通过调用图形驱动程序来完成任务的。图形驱动程序屏蔽了底层驱动的细节,完成底层驱动相关的功能,而不是那么硬件相关的一些功能,如一些画圆,画线的GDI函数。
下面基于已经实现的基于FrameBuffer的驱动程序,讲一些实现上的细节。首先列出的核心数据结构SCREENDEVICE。这里主要是为了讲解方便,所以删除了一些次要的变量或者函数。
清单 5 Native 图形引擎的核心数据结构
typedefstruct _screendevice {
int xres; /* X screen res (real) */
int yres; /* Y screen res (real) */
int planes; /* # planes*/
int bpp; /* # bits per pixel*/
int linelen; /* line length in bytes for bpp 1,2,4,8, line length in pixels forbpp 16, 24, 32*/
int size; /* size of memory allocated*/
gfx_pixelgr_foreground; /* current foregroundcolor */
gfx_pixel gr_background; /* current background color */
int gr_mode;
int flags; /* device flags*/
void * addr; /* address of memory allocated (memdc or fb)*/
PSD(*Open)(PSD psd);
void (*Close)(PSD psd);
void (*SetPalette)(PSD psd,int first,int count,gfx_color *cmap);
void (*GetPalette)(PSD psd,int first,intcount,gfx_color *cmap);
PSD(*AllocateMemGC)(PSD psd);
BOOL (*MapMemGC)(PSD mempsd,int w,int h,int planes,int bpp, intlinelen,int size,void *addr);
void (*FreeMemGC)(PSD mempsd);
void (*FillRect)(PSD psd,int x,int y,int w,int h,gfx_pixel c);
void (*DrawPixel)(PSD psd, int x, int y,gfx_pixel c);
gfx_pixel (*ReadPixel)(PSD psd,int x, int y);
void (*DrawHLine)(PSD psd, int x, int y, int w, gfx_pixel c);
void (*PutHLine) (GAL gal, int x, int y, int w,void* buf);
void (*GetHLine)(GAL gal, int x, int y, int w, void* buf);
void (*DrawVLine)(PSD psd, int x, int y, int w,gfx_pixel c);
void (*PutVLine) (GAL gal, int x, int y, int w, void* buf);
void (*GetVLine) (GAL gal, int x, int y, int w,void* buf);
void (*Blit)(PSD dstpsd, int dstx,int dsty, int w, int h, PSD srcpsd, int srcx, int srcy);
void (*PutBox)( GAL gal, int x, int y, int w, inth, void* buf );
void (*GetBox)( GAL gal, int x, int y, int w, int h, void* buf );
void (*PutBoxMask)( GAL gal, int x, int y, int w,int h, void *buf);
void (*CopyBox)(PSD psd,int x1, int y1, int w, int h, int x2, int y2);
}SCREENDEVICE;
上面PSD是SCREENDEVICE的指针,GAL是GAL接口的数据结构。
我们知道,图形显示有个显示模式的概念,一个像素可以用一位比特表示,也可以用2,4,8,15,16,24,32个比特表示,另外,VGA16标准模式使用平面图形模式,而VESA2.0使用的是线性图形模式。所以即使是同样基于Framebuffer的驱动,不同的模式也要使用不同的驱动函数:画一个1比特的单色点和画一个24位的真彩点显然是不一样的。
所以图形驱动程序使用了子驱动程序的概念来支持各种不同的显示模式,事实上,它们才是最终的功能函数。为了保持数据结构在层次上不至于很复杂,我们通过图形驱动程序的初始函数Open直接将子驱动程序的各功能函数赋到图形驱动程序的接口函数指针,从而初始化结束就使用一个简单的图形驱动接口。下面是子图形驱动程序接口(清单6)。
清单 6 Native 图形引擎的子驱动程序接口
typedefstruct {
int (*Init)(PSD psd);
void (*DrawPixel)(PSD psd, int x, int y,gfx_pixel c);
gfx_pixel (*ReadPixel)(PSD psd,int x, int y);
void (*DrawHLine)(PSD psd, int x, int y, int w, gfx_pixel c);
void (*PutHLine) (GAL gal, int x, int y, int w,void* buf);
void (*GetHLine)(GAL gal, int x, int y, int w, void* buf);
void (*DrawVLine)(PSD psd, int x, int y, int w,gfx_pixel c);
void (*PutVLine) (GAL gal, int x, int y, int w, void* buf);
void (*GetVLine) (GAL gal, int x, int y, int w,void* buf);
void (*Blit)(PSD dstpsd, int dstx,int dsty, int w, int h, PSD srcpsd, int srcx, int srcy);
void (*PutBox)( GAL gal, int x, int y, int w, inth, void* buf );
void (*GetBox)( GAL gal, int x, int y, int w, int h, void* buf );
void (*PutBoxMask)( GAL gal, int x, int y, int w,int h, void *buf);
void (*CopyBox)(PSD psd,int x1, int y1, int w, int h, int x2, int y2);
}SUBDRIVER, *PSUBDRIVER;
可以看到,该接口中除了Init函数指针外,其他的函数指针都与图形驱动程序接口中的函数指针一样。这里的Init函数主要用来完成图形驱动部分与显示模式相关的初始化任务。
下面介绍SCREENDEVICE数据结构,这样基本上就可以清楚图形引擎了。
一个SCREENDEVICE代表一个屏幕设备,它即可以对应物理屏幕设备,也可以对应一个内存屏幕设备,内存屏幕设备的存在主要是为了提高GDI质量,比如我们先在内存生成一幅位图,再画到屏幕上,这样给用户的视觉效果就比较好。
首先介绍几个变量。
xres表示屏幕的宽(以像素为单位);
yres表示屏幕的高(以像素为单位);
planes:当处于平面显示模式时,planes用于记录所使用的平面数,如平面模式相对的时线性模式,此时该变量没有意义。通常将其置为0。
bpp:表示每个像素所使用的比特数,可以为1,2,4,8,15,16,24,32。
linelen:对与1,2,4,8比特每像素模式,它表示一行像素使用的字节数,对于大于8比特每像素模式,它表示一行的像素总数。
size:表示该显示模式下该设备使用的内存数。linelen和size的存在主要是为了方便为内存屏幕设备分配内存。
gr_foreground和gr_background:表示该内存屏幕的前景颜色和背景颜色,主要被一些GDI函数使用。
gr_mode:说明如何将像素画到屏幕上,可选值为:MODE_SETMODE_XOR MODE_OR MODE_AND MODE_MAX,比较常用的是MODE_SET和MODE_XOR
flags:该屏幕设备的一些选项,比较重要的是PSF_MEMORY标志,表示该屏幕设备代表物理屏幕设备还是一个内存屏幕设备。
addr:每个屏幕设备都有一块内存空间用来作为存储像素。addr变量记录了这个空间的起始地址。
下面介绍各接口函数:
Open,Close
基本的初始化和终结函数。前面已经提到,在Open函数里要选择子图形驱动程序,将其实现的函数赋给本PSD结构的函数指针。这里我讲讲基于Frambebuffer的图形引擎的初始化。
fb_open首先打开Framebuffer的设备文件/dev/fb0,然后利用ioctl读出当前Framebuffer的各种信息。填充到PSD结构中。并且根据这些信息选出子驱动程序。程序当前支持fbvga16,fblin16,fblin8,即VGA16标准模式,VESA线性16位模式,VESA线性8位模式。然后将当前终端模式置于图形模式。并保存当前的一些系统信息如调色板信息。最后,系统利用mmap将/dev/fb0映射到内存地址。以后程序访问/dev/fb0就像访问一个数组一样简单。当然,这是对线性模式而言的,如果是平面模式,问题要复杂的多。光从代码来看,平面模式的代码是线性模式的实现的将近一倍。后面的难点分析里将讲解这个问题。
SetPalette,GetPalette
当使用8位或以下的图形模式时,要使用系统调色板。这里是调色板处理函数,它们和WindowsAPI 中的概念类似,linux系统利用ioctl提供了处理调色板的接口。
AllocateMemGC,MapMemGC,FreeMemGC
前面屡次提到内存屏幕的概念,内存屏幕是一个伪屏幕,在对屏幕图形操作过程中,比如移动窗口,我们先生成一个内存屏幕,将物理屏幕的一个区域拷贝到内存屏幕,再拷贝到物理屏幕的新位置,这样就减少了屏幕直接拷贝的延时。AllocateMemGC用于给内存屏幕分配空间,MapMemGC做一些初始化工作,而FreeMemGC则释放内存屏幕。
DrawPixel,ReadPixel,DrawHLine,DrawVLine,FillRect
这些是底层图形函数。分别是画点,读点,画水平线,画竖直线,画一个实心矩形。之所以在底层实现这么多函数,是为了提高效率。图形函数支持多种画图模式,常用的有直接设置,亦或,Alpha混合模式,从而可以支持各种图形效果。
PutHLine,GetHLine,PutVLine,GetVLine,PutBox,GetBox,PutBoxMask
Get*函数用于从屏幕拷贝像素到一块内存区,而Put*函数用于将存放于内存区的像素画到屏幕上。PutBoxMask与PutBox的唯一区别是要画的像素如果是白色,就不会被画到屏幕上,从而达到一种透明的效果。
从上面可以看到,这些函数的第一个参数是GAL类型而不是PSD类型,这是因为它们需要GAL层的信息以便在函数内部实现剪切功能。之所以不和其他函数一样在上层实现剪切,是因为这里的剪切比较特殊。比如PutBox,
在剪切输出域时,要同时剪切在缓冲中待输出的像素:超出剪切域的像素不应该被输出。所以,剪切已经不单纯是对线,矩形等GDI对象的剪切。对像素的剪切当然需要知道像素的格式,这些只是为底层所有,所以为了实现高效的剪切,我们选择在底层实现它们。这里所有的函数都有两个部分:先是剪切,再是读或者写像素。
Blit,CopyBox
Blit用于在不同的屏幕设备(物理的或者内存的)之间拷贝一块像素点,CopyBox则用于在同一屏幕上实现区域像素的拷贝。如果使用的是线性模式,Blit的实现非常简单,直接memcpy就可以了,而CopyBox为了防止覆盖问题,必须根据不同的情况,采用不同的拷贝方式,比如从底到顶底拷贝,当新老位置在同一水平位置并且重复时,则需要利用缓冲间接拷贝。如果使用平面显示模式,这里就比较复杂了。因为内存设备总是采用线性模式的,所以就要判断是物理设备还是内存设备,再分别处理。这也大大地增加了fbvga16实现的代码。
Native输入引擎的实现
4Native 输入引擎的实现
4.1鼠标驱动程序
鼠标驱动程序非常简单,抽象意义上讲,初始化鼠标后,每次用户移动鼠标,就可以得到一个X和Y方向上的位移值,驱动程序内部维护鼠标的当前位置,用户移动了鼠标后,当前位置被加上位移值,并通过上层Cursor支持,反映到屏幕上,用户就会认为鼠标被他正确地“移动”了。
事实上,鼠标驱动程序的实现是利用内核或者其他驱动程序提供的接口来完成任务的。Linux内核驱动程序使用设备文件对大多数硬件进行了抽象,比如,我们眼中的ps/2鼠标就是/dev/psaux,鼠标驱动程序接口如清单 7所示。
清单 7 Native 输入引擎的鼠标驱动程序接口
typedefstruct _mousedevice {
int (*Open)(void);
void (*Close)(void);
int(*GetButtonInfo)(void);
void (*GetDefaultAccel)(int *pscale,int *pthresh);
int (*Read)(int *dx,int *dy,int *dz,int *bp);
void (*Suspend)(void);
void (*Resume)(void);
}MOUSEDEVICE;
现在有各种各样的鼠标,例如ms鼠标,ps/2鼠标,总线鼠标,gpm鼠标,它们的主要差别在于初始化和数据包格式上。
例如,打开一个GPM鼠标非常简单,只要将设备文件打开就可以了,当前终端被切换到图形模式时,GPM服务程序就会把鼠标所有的位移信息放到设备文件中去。
staticint GPM_Open(void)
{
mouse_fd =open(GPM_DEV_FILE, O_NONBLOCK);
if (mouse_fd <0)
return -1;
return mouse_fd;
}
对于PS/2鼠标,不但要打开它的设备文件,还要往该设备文件写入控制字符以使得鼠标能够开始工作。
staticint PS2_Open(void)
{
uint8 initdata_ps2[] = { PS2_DEFAULT,PS2_SCALE11, PS2_ENABLE };
mouse_fd =open(PS2_DEV_FILE, O_RDWR | O_NOCTTY | O_NONBLOCK);
if (mouse_fd < 0)
return -1;
write(mouse_fd, initdata_ps2,sizeof(initdata_ps2));
return mouse_fd;
}
各鼠标的数据包格式是不一样的。而且在读这些数据时,首先要根据内核驱动程序提供的格式读数据,还要注意同步:每次扫描到一个头,才能读后面相应的数据,象Microwindows由于没有同步,在某些情况下,鼠标就会不听“指挥”。
鼠标驱动程序中,还有一个“加速”的概念。程序内部用两个变量:scale和thresh来表示。当鼠标的位移超过thresh时,就会被放大scale倍。这样,最后的位移就是:
dx= thresh + (dx - thresh) * scale;
dy = thresh + (dy - thresh) * scale;
至此,mousedriver 基本上很清楚了,上面的接口函数中GetButtonInfo用来告诉调用者该鼠标支持那些button,suspend和resume函数是用来支持虚屏切换的,下面的键盘驱动程序也一样。
4.2键盘驱动程序
在实现键盘驱动程序中遇到的第一个问题就是使用设备文件/dev/tty还是/dev/tty0。
#echo 1 > /dev/tty0
# echo 1 > /dev/tty
结果都将把1输入到当前终端上。另外,如果从伪终端上运行它们,则第一条指令会将1输出到控制台的当前终端,而第二条指令会把1输出到当前伪终端上。从而tty0表示当前控制台终端,tty表示当前终端(实际是当前进程控制终端的别名而已)。
tty0的设备号是4,0
tty1的设备号是5,0
/dev/tty是和进程的每一个终端联系起来的,/dev/tty的驱动程序所做的只是把所有的请求送到合适的终端。
缺省情况下,/dev/tty是普通用户可读写的,而/dev/tty0则只有超级用户能够读写,主要是基于这个原因,我们目前使用/dev/tty作为设备文件。后面所有有关终端处理的程序的都采用它作为当前终端文件,这样也可以和传统的Unix相兼容。
键盘驱动程序接口如清单8所示。
清单 8 Native 输入引擎的键盘驱动程序接口
typedefstruct _kbddevice {
int (*Open)(void);
void (*Close)(void);
void (*GetModifierInfo)(int *modifiers);
int (*Read)(unsigned char *buf,int *modifiers);
void (*Suspend)(void);
void (*Resume)(void);
}KBDDEVICE;
基本原理非常简单,初始化时打开/dev/tty,以后就从该文件读出所有的数据。由于MiniGUI需要捕获KEY_DOWN和KEY_UP消息,键盘被置于原始(raw)模式。这样,程序从/dev/tty中直接读出键盘的扫描码,比如用户按下A键,就可以读到158,放下,又读到30。原始模式下,程序必须自己记下各键的状态,特别是shift,ctrl,alt,capslock 等,所以程序维护一个数组,记录了所有键盘的状态。
这里说明一下鼠标移动,按键等事件是如何被传送到上层消息队列的。MiniGUI工作在用户态,所以它不可能利用中断这种高效的机制。没有内核驱动程序的支持,它也很难利用信号等Unix系统的IPC机制。MiniGUI可以做到的就是看/dev/tty,/dev/mouse等文件是否有数据可以读。上层通过不断调用GAL_WaitEvent尝试读取这些文件。这也是线程Parser的主要任务。GAL_WaitEvent主要利用了系统调用select这一类Unix系统中地位仅次于ioctl的系统调用完成该功能。并将等待到的事件作为返回值返回。
至此介绍了键盘和鼠标的驱动程序,作为简单的输入设备,它们的驱动是非常简单的。事实上,它们的实现代码也比较少,就是在嵌入式系统中要使用的触摸屏,如果操作系统内核支持,其驱动程序也是非常简单的:它只不过是一种特殊的鼠标。
特定嵌入式系统上图形引擎和输入引擎实现
5特定嵌入式系统上图形引擎和输入引擎实现
如前所述,基于Linux的嵌入式系统,其内核一般具备对FrameBuffer的支持,从而可以利用已有的Native图形引擎。在MiniGUI代码中,可通过调整src/gal/native/native.h中的宏定义而定义函数库中是否包含特定的图形引擎驱动程序(清单9):
清单 9 定义Native引擎的子驱动程序
16 /* define or undefine these macros
17 to include or exclude specific fb driver.
18 */
19 #undef _FBLIN1_SUPPORT
20 // #define_FBLIN1_SUPPORT 1
21
22 #undef _FBLIN2_SUPPORT
23 // #define_FBLIN2_SUPPORT 1
24
22 #undef _FBLIN_2_SUPPORT
23 // #define_FBLIN_2_SUPPORT 1
24
25 //#undef _FBLIN4_SUPPORT
26 #define_FBLIN4_SUPPORT 1
27
28 // #undef _FBLIN8_SUPPORT
29 #define_FBLIN8_SUPPORT 1
30
31 // #undef _FBLIN16_SUPPORT
32 #define_FBLIN16_SUPPORT 1
33
34 #undef _FBLIN24_SUPPORT
35 // #define_FBLIN24_SUPPORT 1
36
37 #undef _FBLIN32_SUPPORT
38 // #define_FBLIN32_SUPPORT 1
39
40 #define HAVETEXTMODE 1 /* =0 for graphics only systems*/
其中,HAVETEXTMODE定义系统是否有文本模式,可将MiniGUI中用来关闭文本模式的代码屏蔽掉。_FBLIN_2_SUPPORT和_FBLIN2_SUPPORT分别用来定义bigendian 和littleendian 的2bpp驱动程序。
对于输入引擎来说,情况就有些不同了。因为目前还没有统一的处理输出设备的接口,而且每个嵌入式系统的输入设备也各不相同,所以,我们通常要针对特定嵌入式系统重新输入引擎。下面的代码就是针对ADS基于StrongARM的嵌入式开发系统编写的输入引擎(清单10):
清单 10 为ADS公司基于StrongARM的嵌入式开发系统编写的输入引擎
30
31 #include <stdio.h>
32 #include <stdlib.h>
33 #include <string.h>
34 #include <unistd.h>
35 #include <sys/io.h>
36 #include <sys/ioctl.h>
37 #include <sys/poll.h>
38 #include <linux/kd.h>
39 #include <sys/types.h>
40 #include <sys/stat.h>
41 #include <fcntl.h>
42
43 #include"common.h"
44 #include "misc.h"
45 #include "ads_internal.h"
46 #include"ial.h"
47 #include "ads.h"
48
49 #ifndef NR_KEYS
50 #define NR_KEYS 128
51 #endif
52
53 static int ts;
54 static int mousex = 0;
55 static int mousey = 0;
56 static POS pos;
57
58 static unsigned charstate[NR_KEYS];
59
60 /************************ Low Level Input Operations **********************/
61 /*
62 * Mouse operations -- Event
63 */
64 static int mouse_update(void)
65 {
66 return 0;
67 }
68
69 static intmouse_getx(void)
70 {
71 return mousex;
72 }
73
74 static intmouse_gety(void)
75 {
76 return mousey;
77 }
78
79 static voidmouse_setposition(int x, int y)
80 {
81 }
82
83 static int mouse_getbutton(void)
84 {
85 return pos.b;
86 }
87
88 static void mouse_setrange(intminx,int miny,int maxx,int maxy)
89 {
90 }
91
92 static int keyboard_update(void)
93 {
94 return 0;
95 }
96
97 static char *keyboard_getstate(void)
98 {
99 return (char *)state;
100 }
101
102 #ifdef _LITE_VERSION
103static int wait_event (int which, int maxfd, fd_set *in, fd_set *out,fd_set *except,
104 struct timeval *timeout)
105 {
106 fd_set rfds;
107 int e;
108
109 if (!in) {
110 in= &rfds;
111 FD_ZERO (in);
112 }
113
114 if (which & IAL_MOUSEEVENT) {
115 FD_SET (ts, in);
116 if (ts > maxfd) maxfd = ts;
117 }
118
119 e =select (maxfd + 1, in, out, except, timeout) ;
120
121 if (e > 0) {
122 if (ts >= 0 && FD_ISSET (ts, in))
123 {
124 FD_CLR (ts, in);
125 read (ts, &pos, sizeof (POS));
126 if ( pos.x !=-1 && pos.y !=-1){
127 mousex = pos.x;
128 mousey = pos.y;
129 }
130 pos.b = ( pos.b > 0 ? 4:0);
131 return IAL_MOUSEEVENT;
132 }
133
134 }else if (e < 0) {
135 return -1;
136 }
137 return0;
138 }
139 #else
140 static int wait_event (int which,fd_set *in, fd_set *out, fd_set *except,
141 struct timeval *timeout)
142 {
143 struct pollfd ufd;
144 if ( (which & IAL_MOUSEEVENT) == IAL_MOUSEEVENT)
145 {
146 ufd.fd = ts;
147 ufd.events = POLLIN;
148 if ( poll (&ufd, 1, timeout) > 0)
149 {
150 read (ts, &pos, sizeof(POS));
151 return IAL_MOUSEEVENT;
152 }
153 }
154 return0;
155 }
156 #endif
157
158 static void set_leds(unsigned int leds)
159 {
160 }
161
162 BOOL InitADSInput(INPUT* input, const char* mdev, const char* mtype)
163{
164 inti;
165
166 ts =open ("/dev/ts", O_RDONLY);
167 if ( ts < 0 ) {
168 fprintf (stderr, "IAL: Can not open touchscreen!\n");
169 return FALSE;
170 }
171
172 for(i = 0; i < NR_KEYS; i++)
173 state[i] = 0;
174
175 input->update_mouse = mouse_update;
176 input->get_mouse_x = mouse_getx;
177 input->get_mouse_y = mouse_gety;
178 input->set_mouse_xy = mouse_setposition;
179 input->get_mouse_button = mouse_getbutton;
180 input->set_mouse_range = mouse_setrange;
181
182 input->update_keyboard = keyboard_update;
183 input->get_keyboard_state = keyboard_getstate;
184 input->set_leds = set_leds;
185
186 input->wait_event = wait_event;
187 mousex = 0;
188 mousey = 0;
189 return TRUE;
190}
191
192 void TermADSInput (void)
193 {
194 if ( ts >= 0 )
195 close(ts);
196 }
197
在上述输入引擎中,完全忽略了键盘相关的函数实现,代码集中在对触摸屏的处理上。显然,输入引擎的编写并不是非常困难的。
小结
6小结
本文详细介绍了MiniGUI的GAL和IAL接口,并以Native图形引擎和输入引擎为例,介绍了具体图形引擎和输入引擎的实现。当然,MiniGUI目前的GAL和IAL接口还有许多不足之处,比如和上层的GDI耦合程度不高,从而对效率有些损失。在MiniGUI将来的开发中,我们将重新设计GDI以及底层的图形引擎接口,以便针对窗口系统进行优化。
开发指南
自MiniGUI从1998年底推出以来,越来越多的人开始选择MiniGUI在Linux上开发实时嵌入式系统。为了帮助嵌入式软件开发人员使用MiniGUI编写出更好的应用程序,我们将撰写一系列文章讲解基于Linux和MiniGUI的嵌入式系统软件开发,并冠名"基于Linux和MiniGUI的嵌入式系统软件开发指南"。
选择MiniGUI-Threads或者MiniGUI-Lite
1:引言
自MiniGUI从1998年底推出以来,越来越多的人开始选择MiniGUI在Linux上开发实时嵌入式系统。MiniGUI系统也逐渐成熟,并在各种嵌入式系统中扮演了重要的角色。为了帮助嵌入式软件开发人员使用MiniGUI编写出更好的应用程序,我们将撰写一系列文章讲解基于Linux和MiniGUI的嵌入式系统软件开发,并冠名"基于Linux和MiniGUI的嵌入式系统软件开发指南"。该系列文章将讲述如何在基于Linux的系统上利用MiniGUI开发具有图形用户界面支持的嵌入式系统软件,其内容不仅仅限于MiniGUI的编程,还会涉及到一些Linux下嵌入式系统软件开发的技巧。系列文章的初步规划如下:
·如何针对特定项目选择MiniGUI-Threads和MiniGUI-Lite
·理解消息循环和窗口过程
·对话框和控件编程
·使用GDI函数
·MiniGUI 和Linux系统调用
·MiniGUI-Lite 与进程间通讯
·将MiniGUI及应用程序移植到特定平台
·利用autoconf接口编写跨平台代码
·如何调试MiniGUI应用程序
本文是该系列文章的第一篇,将讲述如何针对具体项目选择使用MiniGUI-Threads或者MiniGUI-Lite版本,并比较不同版本对系统软件结构的影响。
2:MiniGUI-Threads和MiniGUI-Lite的区别
大家都知道,我们可以将MiniGUI编译成两个截然不同的版本,一个是MiniGUI-Threads,一个是MiniGUI-Lite。这两个版本适用于不同的应用需求。在选择到底使用MiniGUI-Threads还是MiniGUI-Lite之前,我们首先需要了解这两个版本之间的区别。
MiniGUI-Threads是MiniGUI的最初版本。MiniGUI最初为一个工业控制系统开发的,该系统功能单一,但却需要非常高的实时性,因此考虑将MiniGUI开发成一个基于多线程的图形用户界面支持系统。因为在传统的UNIX/Linux系统上,典型的GUI系统(比如X)采用传统的基于UNIX套接字的客户/服务器系统结构。在这种体系结构下,客户建立窗口、绘制等等都要通过套接字传递到服务器,由服务器完成实质工作。这样,系统非常依赖于UNIX套接字通讯。而大家都知道,UNIX套接字的数据传递,要经过内核,然后再传递到另外一个程序。这样,大量的数据在客户/内核/服务器之间传递,从而增加了系统负荷,也占用了许多系统资源。这对许多嵌入式系统,尤其是实时性要求非常高的系统来说,是不可接受的。
为了解决这个问题,MiniGUI首先采用了线程机制(类似WindowsCE),所有的应用程序都运行在同一个地址空间,这样,大大提高了程序之间的通讯效率,并且特别适合于实时性要求非常高的系统。这就是MiniGUI-Threads。基于MiniGUI-Threads的程序,可以具有多个线程,每个线程有不同的功能和任务,并且可以建立各自的窗口,不同的线程之间,可以通过MiniGUI提供的消息传递机制进行事件传送和同步。
但显然,这种基于线程的结构也导致了系统整体的脆弱――如果某个线程因为非法的数据访问而终止运行,则整个进程都将受到影响。不过,这种体系结构对实时控制系统等时间关键的系统来讲,还是非常适合的。
为了解决MiniGUI-Threads版本因为线程而引入的一些问题,同时也为了让MiniGUI更加适合于嵌入式系统,我们决定开发一个MiniGUI-Lite版本。这个版本的开发目的是:
·保持与原先MiniGUI版本在源代码级99%以上的兼容。
·不再使用线程库。
·可以同时运行多个基于MiniGUI-Lite的应用程序,即多个进程,并且提供前后台进程的切换。
显然,要同时满足上述三个目的,如果采用传统的C/S结构对MiniGUI-Threads进行改造,应该不难实现。但前面提到的传统C/S结构的缺陷却无法避免。经过对PDA等嵌入式系统的分析,我们发现,某些PDA产品具有运行多个任务的能力,但同一时刻在屏幕上进行绘制的程序,一般不会超过两个。因此,只要确保将这两个进程的绘制相互隔离,就不需要采用复杂的C/S结构处理多个进程窗口之间的互相剪切。也就是说,在这种产品中,如果采用基于传统C/S结构的多窗口系统,实际是一种浪费。
有了上述认识,我们对MiniGUI-Threads进行了如下简化设计:
每个进程维护自己的主窗口Z序,同一进程创建的主窗口之间互相剪切。也就是说,除这个进程只有一个线程,只有一个消息循环之外,它与原有的MiniGUI版本之间没有任何区别。每个进程在进行屏幕绘制时,不需要考虑其他进程。
建立一个简单的客户/服务器体系,但确保最小化进程间的数据复制功能。因此,在服务器和客户之间传递的数据仅限于输入设备的输入数据,以及客户和服务器之间的某些请求和响应数据。
有一个服务器进程(mginit),它负责初始化一些输入设备,并且通过UNIXDomain 套接字将输入设备的消息发送到前台的MiniGUI-Lite客户进程。
服务器和客户被分别限定在屏幕的某两个不相交矩形内进行绘制,同一时刻,只能有一个客户及服务器进行屏幕绘制。其他客户可继续运行,但屏幕输入被屏蔽。服务器可以利用API接口将某个客户切换到前台。同时,服务器和客户之间采用信号和SystemV 信号量进行同步。
服务器还采用SystemV IPC 机制提供一些资源的共享,包括位图、图标、鼠标、字体等等,以便减少实际内存的消耗。
从传统 C/S窗口系统的角度看,MiniGUI-Lite的这种设计,无法处理达到完整的多窗口支持,这的确是一个结构设计上的不足和缺陷。不过,这实际是MiniGUI-Lite不同于其他窗口系统的一个特征。因为处理每个进程之间的互相剪切问题,将导致客户和服务器之间的通讯量大大增加,但实际上在许多嵌入式系统当中这种处理是没有必要的。在类似PDA的嵌入式系统中,往往各个程序启动后,就独占屏幕进行绘制输出,其他程序根本就没有必要知道它现在的窗口被别的进程剪切了,因为它根本就没有机会输出到屏幕上。所以,在MiniGUI-Lite当中,当一个进程成为最顶层程序时,服务器会保证其输出正常,而当有新的程序成为最顶层程序时,服务器也会保证其他程序不能输出到屏幕上。但这些进程依然在正常执行着,不过,服务器只向最顶层的程序发送外部事件消息。
表 1给出了MiniGUI-Threads和MiniGUI-Lite的区别。从表中总结的区别看来,MiniGUI-Threads适合于功能单一、实时性要求很高的系统,比如工业控制系统;而MiniGUI-Lite适合于功能丰富、结构复杂、显示屏幕较小的系统,比如PDA等信息产品。
表1MiniGUI-Threads 和MiniGUI-Lite的区别
#MiniGUI-Threads
* MiniGUI-Lite
多窗口支持
# 完全
* 不能处理进程间窗口的剪切,但提供进程内多窗口的完全支持
字体支持
# 支持点阵字体(VBF、RBF)和矢量字体(AdobeType1 和TrueType)
* 目前尚不支持对AdobeType1 和TrueType等矢量字体的支持
线程间消息传递
# 通过MiniGUI的消息函数,可在不同的线程之间传递消息
* 未考虑多线程应用,不能直接通过MiniGUI消息函数在不同线程之间传递消息
多线程窗口
# MiniGUI 能够处理不同线程之间的窗口层叠
* 不能处理多线程之间的窗口层叠
其他
# 基于线程的C/S结构,系统健壮性较差,因此要求系统经过严格测试
* 采用UNIXDomain Socket 的基于进程的C/S结构,可建立健壮的软件架构。并提供了方便的高层IPC机制
除上表中列出的不同之外,MiniGUI-Threads和MiniGUI-Lite的API是一致的。
3:MiniGUI-Threads的典型应用和软件架构
本文介绍的基于MiniGUI-Threads典型应用是一个计算机数字控制(CNC)系统。这个系统是由清华大学基于RT-Linux建立的机床控制系统。该系统使用MiniGUI-Threads作为图形用户界面支持系统。图1是该CNC系统的用户界面。
图1清华大学基于RT-Linux和MiniGUI的数控系统主界面
图 2是该系统的架构。在用户层,该系统有三个线程,一个作为GUI主线程存在,另一个作为监视线程监视系统的工作状态,并在该线程建立的窗口上输出状态信息,第三个线程是工作线程,该线程执行加工指令,并通过RT-Linux的实时FIFO和系统的实时模块进行通讯。
图2清华大学基于RT-Linux和MiniGUI的数控系统架构
4:MiniGUI-Lite的典型应用和软件架构
这里介绍的典型应用是一个基于MiniGUI-Lite的PDA。该PDA由国内某公司基于Linux开发,其上可以运行各种PIM程序、浏览器以及各种游戏程序。图3是该PDA的用户界面。
图3某公司开发的基于MiniGUI的PDA软件界面
该系统中的所有应用程序都以Linux进程的形式执行,mginit(即MiniGUI-Lite)提供了输入法支持和应用程序管理功能。当应用程序之间需要通讯时,可以通过MiniGUI-Lite所提供的request/response接口实现。图4是该系统的架构。
图4某公司开发的基于MiniGUI的PDA软件架构
5:小结
本文讲解了MiniGUI-Threads和MiniGUI-Lite之间的区别,并举例说明了基于这两个不同版本的不同软件架构。嵌入式程序开发人员必须明白这两个版本之间的区别,并针对具体应用恰当选择使用哪个版本。
消息循环和窗口过程
引言
我们知道,流行的GUI编程都有一个重要的概念与之相关,即"事件驱动编程"。事件驱动的含义就是,程序的流程不再是只有一个入口和若干个出口的串行执行线路;相反,程序会一直处于一个循环状态,在这个循环当中,程序从外部输入设备获取某些事件,比如用户的按键或者鼠标的移动,然后根据这些事件作出某种的响应,并完成一定的功能,这个循环直到程序接受到某个消息为止。"事件驱动"的底层设施,就是常说的"消息队列"和"消息循环"。本文将具体描述MiniGUI中用来处理消息的几个重要函数,并描述MiniGUI-Threads和MiniGUI-Lite在消息循环实现上的一些不同。
窗口是MiniGUI当中最基本的GUI元素,一旦窗口建立之后,窗口就会从消息队列当中获取属于自己的消息,然后交由它的窗口过程进行处理。这些消息当中,有一些是基本的输入设备事件,而有一些则是与窗口管理相关的逻辑消息。本文将讲述MiniGUI中的窗口建立和销毁过程,并解释了窗口过程的概念以及对一些重要消息的处理。
2消息和消息循环
在MiniGUI中,消息被如下定义(include/window.h):
352typedef struct _MSG
353 {
354 HWND hwnd;
355 int message;
356 WPARAM wParam;
357 LPARAM lParam;
358 #ifdef _LITE_VERSION
359 unsigned int time;
360 #else
361 struct timeval time;
362 #endif
363 POINT pt;
364 #ifndef _LITE_VERSION
365 void* pAdd;
366 #endif
367 }MSG;
368 typedef MSG* PMSG;
一个消息由该消息所属的窗口(hwnd)、消息编号(message)、消息的WPARAM型参数(wParam)以及消息的LPARAM型参数(lParam)组成。消息的两个参数中包含了重要的内容。比如,对鼠标消息而言,lParam中一般包含鼠标的位置信息,而wParam参数中则包含发生该消息时,对应的SHIFT键的状态信息等。对其他不同的消息类型来讲,wParam和lParam也具有明确的定义。当然,用户也可以自定义消息,并定义消息的wParam和lParam意义。为了用户能够自定义消息,MiniGUI定义了MSG_USER宏,可如下定义自己的消息:
#defineMSG_MYMESSAGE1 (MSG_USER + 1)
#define MSG_MYMESSAGE2 (MSG_USER + 2)
用户可以在自己的程序中使用自定义消息,并利用自定义消息传递数据。
在理解消息之后,我们看消息循环。简而言之,消息循环就是一个循环体,在这个循环体中,程序利用GetMessage函数不停地从消息队列中获得消息,然后利用DispatchMessage函数将消息发送到指定的窗口,也就是调用指定窗口的窗口过程,并传递消息及其参数。典型的消息循环如下所示:
while(GetMessage (&Msg, hMainWnd)) {
TranslateMessage (&Msg);
DispatchMessage (&Msg);
}
如上所示,GetMessage函数从hMainWnd窗口所属的消息队列当中获得消息,然后调用TranslateMessage函数将MSG_KEYDOWN和MSG_KEYUP消息翻译成MSG_CHAR消息,最后调用DispatchMessage函数将消息发送到指定的窗口。
在MiniGUI-Threads版本中,每个建立有窗口的GUI线程有自己的消息队列,而且,所有属于同一线程的窗口共享同一个消息队列。因此,GetMessage函数将获得所有与hMainWnd窗口在同一线程中的窗口的消息。
而在MiniGUI-Lite版本中,只有一个消息队列,GetMessage将从该消息队列当中获得所有的消息,而忽略hMainWnd参数。
3几个重要的消息处理函数
除了上面提到的GetMessage和TranslateMessage、DispatchMessage函数以外,MiniGUI支持如下几个消息处理函数。
PostMessage:该函数将消息放到指定窗口的消息队列后立即返回。这种发送方式称为"邮寄"消息。如果消息队列中的邮寄消息缓冲区已满,则该函数返回错误值。在下一个消息循环中,由GetMessage函数获得这个消息之后,窗口才会处理该消息。PostMessage一般用于发送一些非关键性的消息。比如在MiniGUI中,鼠标和键盘消息就是通过PostMessage函数发送的。
SendMessage:该函数和PostMessage函数不同,它在发送一条消息给指定窗口时,将等待该消息被处理之后才会返回。当需要知道某个消息的处理结果时,使用该函数发送消息,然后根据其返回值进行处理。在MiniGUI-Threads当中,如果发送消息的线程和接收消息的线程不是同一个线程,发送消息的线程将阻塞并等待另一个线程的处理结果,然后继续运行;否则,SendMessage函数将直接调用接收消息窗口的窗口过程函数。MiniGUI-Lite则和上面的第二种情况一样,直接调用接收消息窗口的窗口过程函数。
SendNotifyMessage:该函数和PostMessage消息类似,也是不等待消息被处理即返回。但和PostMessage消息不同,通过该函数发送的消息不会因为缓冲区满而丢失,因为系统采用链表的形式处理这种消息。通过该函数发送的消息一般称为"通知消息",一般用来从控件向其父窗口发送通知消息。
PostQuitMessage:该消息在消息队列中设置一个QS_QUIT标志。GetMessage在从指定消息队列中获取消息时,会检查该标志,如果有QS_QUIT标志,GetMessage消息将返回FALSE,从而可以利用该返回值终止消息循环。
4MiniGUI-Threads 和MiniGUI-Lite在消息处理上的不同
表1总结了MiniGUI-Threads和MiniGUI-Lite在消息处理上的不同
表1MiniGUI-Threads 和MiniGUI-Lite在消息处理上的不同
#MiniGUI-Threads
* MiniGUI-Lite
多消息队列
# 每个创建窗口的线程拥有独立的消息队列
* 只有一个消息队列。所有窗口共享一个消息队列。除非嵌套消息循环,否则一个程序中只有一个消息循环。
内建多线程处理
# 是。可以自动处理跨线程的消息传递
* 不能。从一个线程向另外一个线程发送或者邮寄消息时,必须通过互斥处理保护消息队列。
其他
# 可以利用PostSyncMessage函数跨线程发送消息,并等待消息的处理结果
* 不能使用PostSyncMessage、SendAsynMessage等消息。
5窗口的建立和销毁
5.1窗口的建立
我们知道,MiniGUI的API类似Win32的API。因此,窗口的建立过程和Windows程序基本类似。不过也有一些差别。首先我们回顾一下Windows应用程序的框架:
·在WinMain() 中创建窗口,使用以下步骤:创建窗口类、登记窗口类、创建并显示窗口、启动消息循环。
·在WndProc() 中,负责对发到窗口中的各种消息进行响应。
·在MiniGUI中也同样要有这两个函数。不过稍微有点不同。程序的入口函数名字叫MiniGUIMain(),它负责创建程序的主窗口。在建立主窗口之后,程序进入消息循环。
在 Win32程序中,在建立一个主窗口之前,程序首先要注册一个窗口类,然后创建一个属于该窗口类的主窗口。MiniGUI却没有在主窗口中使用窗口类的概念。在MiniGUI程序中,首先初始化一个MAINWINCREATE结构,该结构中元素的含义是:
CreateInfo.dwStyle:窗口风格
CreateInfo.spCaption:窗口的标题
CreateInfo.dwExStyle:窗口的附加风格
CreateInfo.hMenu:附加在窗口上的菜单句柄
CreateInfo.hCursor:在窗口中所使用的鼠标光标句柄
CreateInfo.hIcon:程序的图标
CreateInfo.MainWindowProc:该窗口的消息处理函数指针
CreateInfo.lx:窗口左上角相对屏幕的绝对横坐标,以象素点表示
CreateInfo.ty:窗口左上角相对屏幕的绝对纵坐标,以象素点表示
CreateInfo.rx:窗口的长,以象素点表示
CreateInfo.by:窗口的高,以象素点表示
CreateInfo.iBkColor:窗口背景颜色
CreateInfo.dwAddData:附带给窗口的一个 32位值
CreateInfo.hHosting:窗口消息队列所属
其中有如下几点要特别说明:
CreateInfo.dwAddData:在程序编制过程中,应该尽量减少静态变量,但是如何不使用静态变量而给窗口传递参数呢?这时可以使用这个域。该域是一个32位的值,因此可以把所有需要传递给窗口的参数编制成一个结构,而将结构的指针赋予该域。在窗口过程中,可以使用GetWindowAdditionalData函数获取该指针,从而获得所需要传递的参数。
CreateInfo.hHosting:该域表示的是将要建立的主窗口使用哪个主窗口的消息队列。使用其他主窗口消息队列的主窗口,我们称为"被托管"的主窗口。当然,这只在MiniGUI-Threads版本中有效。
MainWinProc函数负责处理窗口消息。这个函数就是主窗口的"窗口过程"。窗口过程一般有四个入口参数,第一个是窗口句柄,第二个是消息类型,第三个和第四个是消息的两个参数。
在准备好MAINWINCREATE结构之后,就可以调用CreateMainWindow函数建立主窗口了。在建立主窗口之后,典型的程序将进入消息循环。如下所示:
intMiniGUIMain (int args, const char* arg[])
{
MSG Msg;
MAINWINCREATE CreateInfo;
HWND hWnd;
//初始化MAINWINCREATE结构
CreateInfo.dwStyle = WS_VISIBLE | WS_VSCROLL | WS_HSCROLL |WS_CAPTION;
CreateInfo.spCaption= "MiniGUIstep three";
CreateInfo.dwExStyle =WS_EX_NONE;
CreateInfo.hMenu =createmenu();
CreateInfo.hCursor =GetSystemCursor(0);
CreateInfo.hIcon = 0;
CreateInfo.MainWindowProc = MainWinProc;
CreateInfo.lx = 0;
CreateInfo.ty = 0;
CreateInfo.rx = 640;
CreateInfo.by = 480;
CreateInfo.iBkColor = COLOR_lightwhite;
CreateInfo.dwAddData = 0;
CreateInfo.hHosting =HWND_DESKTOP;
//建立主窗口
hWnd = CreateMainWindow(&CreateInfo);
if(hWnd == HWND_INVALID)
return 0;
//显示主窗口
ShowWindow (hWnd, SW_SHOWNORMAL);
// 进入消息循环
while (GetMessage(&Msg, hWnd)) {
TranslateMessage (&Msg);
DispatchMessage(&Msg);
}
MainWindowThreadCleanup(hWnd);
return 0;
}
注意,和Windows程序不同的是,在退出消息循环之后,还要调用一个函数,即MainWindowThreadCleaup函数。该函数的工作是销毁主窗口的消息队列,一般在线程或者进程的最后调用。
5.2窗口的销毁
要销毁一个主窗口,可以利用DestroyMainWindow(hWnd) 函数。该函数将销毁主窗口,但不会销毁主窗口所使用的消息队列,而要使用MainWindowThreadCleaup最终清除主窗口所使用的消息队列。
一般而言,一个主窗口过程在接收到MSG_CLOSE消息之后会销毁主窗口,并调用PostQuitMessage消息终止消息循环。如下所示:
caseMSG_CLOSE:
// 销毁窗口使用的资源
DestroyLogFont (logfont1);
DestroyLogFont(logfont2);
DestroyLogFont (logfont3);
//销毁子窗口
DestroyWindow(hWndButton);
DestroyWindow(hWndEdit);
// 销毁主窗口
DestroyMainWindow (hWnd);
// 发送MSG_QUIT消息
PostQuitMessage(hWnd);
return 0;
6几个重要消息
在窗口(包括主窗口和子窗口在内)的生存周期当中,有几个重要的消息需要仔细处理。下面描述这些消息的概念和典型处理。
6.1MSG_NCCREATE
该消息在MiniGUI建立主窗口的过程中发送到窗口过程。lParam中包含了由CreateMainWindow传递进入的pCreateInfo结构指针。您可以在该消息的处理过程中修改pCreateInfo结构中的某些值。
6.2MSG_SIZECHANGING
该消息窗口尺寸发生变化时,或者建立窗口时发送到窗口过程,用来确定窗口大小。wParam包含预期的窗口尺寸值,而lParam用来保存结果值。MiniGUI的默认处理是,
caseMSG_SIZECHANGING:
memcpy ((PRECT)lParam,(PRECT)wParam, sizeof (RECT));
return 0;
你可以截获该消息的处理,从而让即将创建的窗口位于指定的位置,或者具有固定的大小,比如在SPINBOX控件中,就处理了该消息,使之具有固定的大小:
caseMSG_SIZECHANGING:
{
const RECT* rcExpect =(const RECT*) wParam;
RECT* rcResult = (RECT*)lPraram;
rcResult->left= rcExpect->left;
rcResult->top =rcExpect->top;
rcResult->right =rcExpect->left + _WIDTH;
rcResult->bottom = rcExpect->left + _HEIGHT;
return 0;
}
6.3MSG_CHANGESIZE
在确立窗口大小之后,该消息被发送到窗口过程,用来通知确定之后的窗口大小。wParam包含了窗口大小RECT的指针。注意应用程序应该将该消息传递给MiniGUI进行默认处理。
6.4MSG_SIZECHANGED
该消息用来确定窗口客户区的大小,和MSG_SIZECHANGING消息类似。wParam参数包含窗口大小信息,lParam参数是用来保存窗口客户区大小的RECT指针,并且具有默认值。如果该消息的处理返回非零值,则将采用lParam当中包含的大小值作为客户区的大小;否则,将忽略该消息的处理。比如在SPINBOX控件中,就处理了该消息,并使客户区占具所有的窗口范围:
caseMSG_SIZECHANGED
{
RECT* rcClient = (RECT*)lPraram;
rcClient->right= rcClient->left + _WIDTH;
rcClient->bottom = rcClient->top + _HEIGHT;
return 0;
}
6.5MSG_CREATE
该消息在建立好的窗口成功添加到MiniGUI的窗口管理器之后发送到窗口过程。这时,应用程序可以在其中创建子窗口。如果该消息返回非零值,则将销毁新建的窗口。注意,在MSG_NCCREATE消息被发送时,窗口尚未正常建立,所以不能在MSG_NCCREATE消息中建立子窗口。
6.6MSG_PAINT
该消息在需要进行窗口重绘时发送到窗口过程。MiniGUI通过判断窗口是否含有无效区域来确定是否需要重绘。当窗口在初始显示、从隐藏状态变化为显示状态、从部分不可见到可见状态,或者应用程序调用InvalidateRect函数使某个矩形区域变成无效时,窗口将具有特定的无效区域。这时,MiniGUI将在处理完所有的邮寄消息、通知消息之后处理无效区域,并向窗口过程发送MSG_PAINT消息。该消息的典型处理如下:
caseMSG_PAINT:
{
HDC hdc;
hdc= BeginPaint (hWnd);
//使用hdc绘制窗口
...
EndPaint(hWnd, hdc);
break;
}
6.7MSG_DESTROY
该消息在应用程序调用DestroyMainWindow或者DestroyWindow时发送到窗口过程当中,用来通知系统即将销毁一个窗口。如果该消息的处理返回非零值,则将取消销毁过程。
7Hello, World
在这个小节当中,我们给出一个简单的示例程序,该程序在窗口中打印"Hello,world!":
#include<stdio.h>
#include <stdlib.h>
#include <string.h>
#include<minigui/common.h>
#include <minigui/minigui.h>
#include<minigui/gdi.h>
#include <minigui/window.h>
staticint HelloWinProc (HWND hWnd, int message, WPARAM wParam, LPARAMlParam)
{
HDC hdc;
switch(message) {
caseMSG_PAINT:
hdc = BeginPaint (hWnd);
TexOut (hdc, 0, 0, "Hello, world!");
EndPaint (hWnd, hdc);
break;
caseMSG_CLOSE:
DestroyMainWindow (hWnd);
PostQuitMessage (hWnd);
return 0;
}
returnDefaultMainWinProc(hWnd, message, wParam, lParam);
}
staticvoid InitCreateInfo (PMAINWINCREATE pCreateInfo)
{
pCreateInfo->dwStyle = WS_CAPTION | WS_VISIBLE;
pCreateInfo->dwExStyle = 0;
pCreateInfo->spCaption = "Hello, world!" ;
pCreateInfo->hMenu = 0;
pCreateInfo->hCursor= GetSystemCursor (0);
pCreateInfo->hIcon =0;
pCreateInfo->MainWindowProc =HelloWinProc;
pCreateInfo->lx = 0;
pCreateInfo->ty = 0;
pCreateInfo->rx =320;
pCreateInfo->by = 240;
pCreateInfo->iBkColor = PIXEL_lightwhite;
pCreateInfo->dwAddData = 0;
pCreateInfo->hHosting = HWND_DESKTOP;
}
intMiniGUIMain (int args, const char* arg[])
{
MSG Msg;
MAINWINCREATE CreateInfo;
HWND hMainWnd;
#ifdef_LITE_VERSION
SetDesktopRect (0, 0, 800,600);
#endif
InitCreateInfo(&CreateInfo);
hMainWnd= CreateMainWindow (&CreateInfo);
if(hMainWnd == HWND_INVALID)
return -1;
while(GetMessage (&Msg, hMainWnd)) {
DispatchMessage (&Msg);
}
MainWindowThreadCleanup(hMainWnd);
return 0;
}
很显然,这是一个非常简单的程序。该程序使用了MiniGUI的默认过程来处理我们前面提到的许多消息,而仅仅处理了MSG_PAINT和MSG_CLOSE两条消息。当用户单击标题栏上的关闭按钮时,MiniGUI将发送MSG_CLOSE到窗口过程。这时,应用程序就可以销毁窗口,并终止消息循环,最终退出程序。
8小结
本文描述了MiniGUI中与消息相关的几个重要函数,并讲述了MiniGUI-Threads和MiniGUI-Lite在消息机制实现上的几个不同。本文还讲述了在MiniGUI中的窗口建立和销毁过程,并解释了窗口过程的概念以及一些重要消息的处理。最后,本文给出了一个简单的MiniGUI的示例程序,该程序建立窗口,并在其中打印"Hello,world!"。
对话框和控件编程
1引言
对话框编程是一个快速构建用户界面的技术。通常,我们编写简单的图形用户界面时,可以通过调用CreateWindow函数直接创建所有需要的子窗口,即控件。但在图形用户界面比较复杂的情况下,每建立一个控件就调用一次CreateWindow函数,并传递许多复杂参数的方法很不可取。主要原因之一,就是程序代码和用来建立控件的数据混在一起,不利于维护。为此,一般的GUI系统都会提供一种机制,利用这种机制,通过指定一个模板,GUI系统就可以根据此模板建立相应的主窗口和控件。MiniGUI也提供这种方法,通过建立对话框模板,就可以建立模态或者非模态的对话框。
本文首先讲解组成对话框的基础,即控件的基本概念,然后讲解对话模板的定义,并说明模态和非模态对话框之间的区别以及编程技术。
2控件和控件类
许多人对控件(或者部件,widget)的概念已经相当熟悉了。控件可以理解为主窗口中的子窗口。这些子窗口的行为和主窗口一样,即能够接收键盘和鼠标等外部输入,也可以在自己的区域内进行输出――只是它们的所有活动被限制在主窗口中。MiniGUI也支持子窗口,并且可以在子窗口中嵌套建立子窗口。我们将MiniGUI中的所有子窗口均称为控件。
在 Windows或XWindow中,系统会预先定义一些控件类,当利用某个控件类创建控件之后,所有属于这个控件类的控件均会具有相同的行为和显示。利用这些技术,可以确保一致的人机操作界面,而对程序员来讲,可以像搭积木一样地组建图形用户界面。MiniGUI使用了控件类和控件的概念,并且可以方便地对已有控件进行重载,使得其有一些特殊效果。比如,需要建立一个只允许输入数字的编辑框时,就可以通过重载已有编辑框而实现,而不需要重新编写一个新的控件类。
如果读者曾经编写过Windows应用程序的话,应该记得在建立一个窗口之前,必须确保系统中存在新窗口所对应的窗口类。在Windows中,程序所建立的每个窗口,都对应着某种窗口类。这一概念和面向对象编程中的类、对象的关系类似。借用面向对象的术语,Windows中的每个窗口实际都是某个窗口类的一个实例。在XWindow 编程中,也有类似的概念,比如我们建立的每一个Widget,实际都是某个Widget类的实例。
这样,如果程序需要建立一个窗口,就首先要确保选择正确的窗口类,因为每个窗口类决定了对应窗口实例的表象和行为。这里的表象指窗口的外观,比如窗口边框宽度,是否有标题栏等等,行为指窗口对用户输入的响应。每一个GUI系统都会预定义一些窗口类,常见的有按钮、列表框、滚动条、编辑框等等。如果程序要建立的窗口很特殊,就需要首先注册一个窗口类,然后建立这个窗口类一个实例。这样就大大提高了代码的可重用性。
在 MiniGUI中,我们认为主窗口通常是一种比较特殊的窗口。因为主窗口代码的可重用性一般很低,如果按照通常的方式为每个主窗口注册一个窗口类的话,则会导致额外不必要的存储空间,所以我们并没有在主窗口提供窗口类支持。但主窗口中的所有子窗口,即控件,均支持窗口类(控件类)的概念。MiniGUI提供了常用的预定义控件类,包括按钮(包括单选钮、复选钮)、静态框、列表框、进度条、滑块、编辑框等等。程序也可以定制自己的控件类,注册后再创建对应的实例。表1给出了MiniGUI预先定义的控件类和相应类名称定义。
表1MiniGUI 预定义的控件类和对应类名称
控件类类名称 宏定义 备注
静态框"static"CTRL_STATIC
按钮"button"CTRL_BUTTON
列表框"listbox"CTRL_LISTBOX
进度条"progressbar"CTRL_PRORESSBAR
滑块"trackbar"CTRL_TRACKBAR
单行编辑框"edit"、"sledit"CTRL_EDIT、CTRL_SLEDIT
多行编辑框"medit"、"mledit"CTRL_MEDIT、CTRL_MLEDIT
工具条"toolbar"CTRL_TOOLBAR
菜单按钮"menubutton"CTRL_MENUBUTTON
树型控件"treeview"CTRL_TREEVIEW 包含在mgext库,即MiniGUI扩展库中。
月历控件"monthcalendar"CTRL_MONTHCALENDAR 同上
旋钮控件"spinbox"CTRL_SPINBOX 同上
在 MiniGUI中,通过调用CreateWindow函数,可以建立某个控件类的一个实例。控件类既可以是表1中预定义MiniGUI控件类,也可以是用户自定义的控件类。与CreateWindow函数相关的几个函数的原型如下(include/window.h):
904HWND GUIAPI CreateWindowEx (const char* spClassName, const char*spCaption,
905 DWORD dwStyle, DWORD dwExStyle, int id,
906 int x, int y, int w, int h, HWND hParentWnd, DWORD dwAddData);
907BOOL GUIAPI DestroyWindow (HWND hWnd);
908
909 #defineCreateWindow(class_name, caption, style, id, x, y, w, h, parent,add_data) \
910 CreateWindowEx(class_name, caption, style, 0, id, x, y, w, h, parent,add_data)
CreateWindow函数建立一个子窗口,即控件。它指定了控件类、控件标题、控件风格,以及窗口的初始位置和大小。该函数同时指定子窗口的父窗口。CreateWindowEx函数的功能和CreateWindow函数一致,不过,可以通过CreateWindowEx函数指定控件的扩展风格。DestroyWindow函数用来销毁用上述两个函数建立的控件或者子窗口。
清单 1中的程序,利用预定义控件类创建控件。其中hStaticWnd1是建立在主窗口hWnd中的静态框;hButton1、hButton2、hEdit1、hStaticWnd2则是建立在hStaicWnd1内部的几个控件,并作为hStaticWnd1的子控件而存在,建立了两个按钮、一个编辑框和一个静态按钮;而hEdit2是hStaicWnd2的子控件,是hStaticWnd1的子子控件。
清单1利用预定义控件类创建控件
#defineIDC_STATIC1 100
#define IDC_STATIC2 150
#define IDC_BUTTON1 110
#defineIDC_BUTTON2 120
#define IDC_EDIT1 130
#define IDC_EDIT2 140
intControlTestWinProc (HWND hWnd, int message, WPARAM wParam, LPARAMlParam)
{
staticHWND hStaticWnd1, hStaticWnd2, hButton1, hButton2, hEdit1,hEdit2;
switch(message) {
caseMSG_CREATE:
{
hStaticWnd1 = CreateWindow(CTRL_STATIC,
"This is a staticcontrol",
WS_CHILD | SS_NOTIFY | SS_SIMPLE | WS_VISIBLE |WS_BORDER,
IDC_STATIC1,
10, 10, 180, 300, hWnd, 0);
hButton1 = CreateWindow(CTRL_BUTTON,
"Button1",
WS_CHILD | BS_PUSHBUTTON |WS_VISIBLE,
IDC_BUTTON1,
20, 20, 80, 20, hStaticWnd1, 0);
hButton2 = CreateWindow(CTRL_BUTTON,
"Button2",
WS_CHILD | BS_PUSHBUTTON |WS_VISIBLE,
IDC_BUTTON2,
20, 50, 80, 20, hStaticWnd1, 0);
hEdit1 = CreateWindow(CTRL_EDIT,
"Edit Box 1",
WS_CHILD | WS_VISIBLE |WS_BORDER,
IDC_EDIT1,
20, 80, 100, 24, hStaticWnd1, 0);
hStaticWnd2 = CreateWindow(CTRL_STATIC,
"This is child staticcontrol",
WS_CHILD | SS_NOTIFY | SS_SIMPLE | WS_VISIBLE |WS_BORDER,
IDC_STATIC1,
20, 110, 100, 50, hStaticWnd1, 0);
hEdit2 = CreateWindow(CTRL_EDIT,
"Edit Box 2",
WS_CHILD | WS_VISIBLE |WS_BORDER,
IDC_EDIT2,
0, 20, 100, 24, hStaticWnd2, 0);
break;
}
.......
}
returnDefaultMainWinProc (hWnd, message, wParam, lParam);
}
用户也可以通过RegisterWindowClass函数注册自己的控件类,并建立该控件类的控件实例。如果程序不再使用某个自定义的控件类,则应该使用UnregisterWindowClass函数注销自定义的控件类。上述两个函数以及和窗口类相关函数的原型如下(include/window.h):
897BOOL GUIAPI RegisterWindowClass (PWNDCLASS pWndClass);
898 BOOLGUIAPI UnregisterWindowClass (const char* szClassName);
899 char*GUIAPI GetClassName (HWND hWnd);
900 BOOL GUIAPIGetWindowClassInfo (PWNDCLASS pWndClass);
901 BOOL GUIAPISetWindowClassInfo (const WNDCLASS* pWndClass);
RegisterWindowClass通过pWndClass结构注册一个控件类;UnregisterWindowClass函数则注销指定的控件类;GetClassName活得窗口的对应窗口类名称,对主窗口而言,窗口类名称为"MAINWINDOW";GetWindowClassInfo分别用来获取和指定特定窗口类的属性。
清单 2中的程序,定义并注册了一个自己的控件类。该控件用来显示安装程序的步骤信息,MSG_SET_STEP_INFO消息用来定义该控件中显示的所有步骤信息,包括所有步骤名称及其简单描述。MSG_SET_CURR_STEP消息用来指定当前步骤,控件将高亮显示当前步骤。
清单2定义并注册自定义控件类
#defineSTEP_CTRL_NAME "mystep"
#define MSG_SET_STEP_INFO (MSG_USER + 1)
#define MSG_SET_CURR_STEP (MSG_USER +2)
static int StepControlProc (HWND hwnd,
int message, WPARAM wParam, LPARAM lParam)
{
HDC hdc;
HELPWININFO* info;
switch(message) {
case MSG_PAINT:
hdc = BeginPaint (hwnd);
/* 获取步骤控件信息*/
info = (HELPWININFO*)GetWindowAdditionalData (hwnd);
/* 绘制步骤内容*/
......
EndPaint (hwnd,hdc);
break;
/* 控件自定义的消息:用来设置步骤信息*/
case MSG_SET_STEP_INFO:
SetWindowAdditionalData (hwnd, (DWORD)lParam);
InvalidateRect (hwnd, NULL, TRUE);
break;
/* 控件自定义的消息:用来设置当前步骤信息*/
case MSG_SET_CURR_STEP:
InvalidateRect (hwnd, NULL, FALSE);
break;
case MSG_DESTROY:
break;
}
returnDefaultControlProc (hwnd, message, wParam, lParam);
}
staticBOOL RegisterStepControl ()
{
intresult;
WNDCLASS StepClass;
StepClass.spClassName = STEP_CTRL_NAME;
StepClass.dwStyle = 0;
StepClass.hCursor = GetSystemCursor(IDC_ARROW);
StepClass.iBkColor = COLOR_lightwhite;
StepClass.WinProc = StepControlProc;
return RegisterWindowClass(&StepClass);
}
static void UnregisterStepControl ()
{
UnregisterWindowClass (STEP_CTRL_NAME);
}
3控件子类化
采用控件类和控件实例的结构,不仅可以提高代码的可重用性,而且还可以方便地对已有控件类进行扩展。比如,在需要建立一个只允许输入数字的编辑框时,就可以通过重载已有编辑框控件类而实现,而不需要重新编写一个新的控件类。在MiniGUI中,这种技术称为子类化或者窗口派生。子类化的方法有三种:
·一种是对已经建立的控件实例进行子类化,子类化的结果是只影响这一个控件实例;
·一种是对某个控件类进行子类化,将影响其后创建的所有该控件类的控件实例;
·最后一种是在某个控件类的基础上新注册一个子类化的控件类,不会影响原有控件类。在Windows中,这种技术又称为超类化。
在 MiniGUI中,控件的子类化实际是通过替换已有的窗口过程实现的。清单3中的代码就通过控件类创建了两个子类化的编辑框,一个只能输入数字,而另一个只能输入字母:
清单3控件的子类化
#defineIDC_CTRL1 100
#define IDC_CTRL2 110
#define IDC_CTRL3 120
#defineIDC_CTRL4 130
#define MY_ES_DIGIT_ONLY 0x0001
#define MY_ES_ALPHA_ONLY 0x0002
staticWNDPROC old_edit_proc;
static int RestrictedEditBox (HWND hwnd,int message, WPARAM wParam, LPARAM lParam)
{
if (message == MSG_CHAR) {
DWORD my_style = GetWindowAdditionalData (hwnd);
/* 确定被屏蔽的按键类型*/
if ((my_style & MY_ES_DIGIT_ONLY) && (wParam < '0' ||wParam > '9'))
return 0;
else if(my_style & MY_ES_ALPHA_ONLY)
if (!((wParam >= 'A' && wParam <= 'Z') || (wParam >='a' && wParam <= 'z')))
/* 收到被屏蔽的按键消息,直接返回*/
return 0;
}
/*由老的窗口过程处理其余消息*/
return (*old_edit_proc) (hwnd, message, wParam, lParam);
}
staticint ControlTestWinProc (HWND hWnd, int message, WPARAM wParam, LPARAMlParam)
{
switch (message) {
case MSG_CREATE:
{
HWND hWnd1, hWnd2, hWnd3;
CreateWindow (CTRL_STATIC, "Digit-only box:", WS_CHILD |WS_VISIBLE | SS_RIGHT, 0,
10, 10, 180, 24, hWnd, 0);
hWnd1 = CreateWindow (CTRL_EDIT, "", WS_CHILD | WS_VISIBLE| WS_BORDER, IDC_CTRL1,
200, 10, 180, 24, hWnd, MY_ES_DIGIT_ONLY);
CreateWindow (CTRL_STATIC, "Alpha-only box:", WS_CHILD |WS_VISIBLE | SS_RIGHT, 0,
10, 40, 180, 24, hWnd, 0);
hWnd2 = CreateWindow (CTRL_EDIT, "", WS_CHILD | WS_BORDER |WS_VISIBLE, IDC_CTRL2,
200, 40, 180, 24, hWnd, MY_ES_ALPHA_ONLY);
CreateWindow (CTRL_STATIC, "Normal edit box:", WS_CHILD |WS_VISIBLE | SS_RIGHT, 0,
10, 70, 180, 24, hWnd, 0);
hWnd3 = CreateWindow (CTRL_EDIT, "", WS_CHILD | WS_BORDER |WS_VISIBLE, IDC_CTRL2,
200, 70, 180, 24, hWnd, MY_ES_ALPHA_ONLY);
CreateWindow ("button", "Close", WS_CHILD |BS_PUSHBUTTON | WS_VISIBLE, IDC_CTRL4,
100, 100, 60, 24, hWnd, 0);
/* 用自定义的窗口过程替换编辑框的窗口过程,并保存老的窗口过程。*/
old_edit_proc = SetWindowCallbackProc (hWnd1,RestrictedEditBox);
SetWindowCallbackProc (hWnd2, RestrictedEditBox);
break;
}
......
}
return DefaultMainWinProc (hWnd, message,wParam, lParam);
}
4对话框和对话框模板
在MiniGUI中,对话框是一类特殊的主窗口,这种主窗口只关注与用户的交互――向用户提供输出信息,但更多的是用于用户输入。对话框可以理解为子类化之后的主窗口类。它针对对话框的特殊性(即用户交互)进行了特殊设计。比如用户可以使用TAB键遍历控件、可以利用ENTER键表示默认输入等等。在MiniGUI当中,在建立对话框之前,首先需要定义一个对话框模板,该模板中定义了对话框本身的一些属性,比如位置和大小等等,同时定义了对话框中所有控件的初始信息,包括位置、大小、风格等等。在MiniGUI中,用两个结构来表示对话框模板(src/window.h):
1172typedef struct
1173 {
1174 char* class_name; // control class
1175 DWORD dwStyle; // control style
1176 int x, y, w, h; // control position in dialog
1177 int id; // control identifier
1178 const char*caption; // control caption
1179 DWORD dwAddData; // additional data
1180
1181 DWORD dwExStyle; // control extended style
1182 } CTRLDATA;
1183 typedefCTRLDATA* PCTRLDATA;
1184
1185 typedef struct
1186{
1187 DWORD dwStyle; // dialog box style
1188 DWORD dwExStyle; // dialog box extended style
1189 int x, y, w,h; // dialog box position
1190 const char*caption; // dialog box caption
1191 HICON hIcon; // dialog box icon
1192 HMENU hMenu; // dialog box menu
1193 int controlnr; // number of controls
1194 PCTRLDATA controls; // poiter to control array
1195 DWORD dwAddData; // addtional data, must be zero
1196 } DLGTEMPLATE;
1197typedef DLGTEMPLATE* PDLGTEMPLATE;
1198
结构CTRLDATA用来定义控件,DLGTEMPLATE用来定义对话框本身。在程序中,应该首先利用CTRLDATA定义对话框中所有的控件,并用数组表示。控件在该数组中的顺序,也就是对话框中用户按TAB键时的控件切换顺序。然后定义对话框,指定对话框中的控件数目,并指定DLGTEMPLATE结构中的controls指针指向定义控件的数组。如清单4所示。
清单4对话框模板的定义
DLGTEMPLATEDlgInitProgress =
{
WS_BORDER |WS_CAPTION,
WS_EX_NONE,
120, 150, 400, 130,
"VAM-CNC 正在进行初始化,
0, 0,
3, NULL,
0
};
CTRLDATACtrlInitProgress [] =
{
{
"static",
WS_VISIBLE | SS_SIMPLE,
10, 10, 380, 16,
IDC_PROMPTINFO,
"正在...",
0
},
{
"progressbar",
WS_VISIBLE,
10, 40,380, 20,
IDC_PROGRESS,
NULL,
0
},
{
"button",
WS_TABSTOP | WS_VISIBLE | BS_DEFPUSHBUTTON,
170, 70, 60, 25,
IDOK,
"确定",
0
}
};
在定义了对话框模板数据之后,需要定义对话框的回调函数,并调用DialogBoxIndirectParam函数建立对话框,如清单5所示,所建立的对话框如图1所示。
清单5定义对话框回调函数,并建立对话框
/*定义对话框回调函数*/
staticint InitDialogBoxProc (HWND hDlg, int message, WPARAM wParam, LPARAMlParam)
{
switch (message) {
case MSG_INITDIALOG:
return 1;
caseMSG_COMMAND:
switch(wParam) {
caseIDOK:
caseIDCANCEL:
EndDialog (hDlg, wParam);
break;
}
break;
}
returnDefaultDialogProc (hDlg, message, wParam, lParam);
}
staticvoid InitDialogBox (HWND hWnd)
{
/*将对话框和控件数组关联起来*/
DlgInitProgress.controls = CtrlInitProgress;
DialogBoxIndirectParam(&DlgInitProgress, hWnd, InitDialogBoxProc, 0L);
}
图1清单5程序建立的对话框
DialogBoxIndirectParam以及相关函数的原型如下:
1203int GUIAPI DialogBoxIndirectParam (PDLGTEMPLATEpDlgTemplate,
1204 HWND hOwner, WNDPROC DlgProc, LPARAM lParam);
1205 BOOL GUIAPIEndDialog (HWND hDlg, int endCode);
1206 void GUIAPIDestroyAllControls (HWND hDlg);
在DialogBoxIndirectParam中,需要指定对话框模板(pDlgTemplate)、对话框的托管主窗口句柄(hOwner)、对话框回调函数地址(DlgProc),以及ud戕?a]dd?dd/)c dd dd? 秋诂发送到对话框回调函数的。该消息的lParam参数包含了由DialogBoxIndirectParam函数的第四个参数传递到对话框回调函数的值。用户可以利用该值进行对话框的初始化,或者保存起来以备后用。例如,清单7中的程序将MSG_INITDIALOG消息的lParam参数保存到了对话框窗口句柄的附加数据中,这样可以确保在任何需要的时候,方便地从对话框窗口的附加数据中获取这一数据。
staticint DepInfoBoxProc (HWND hDlg, int message, WPARAM wParam, LPARAMlParam)
{
struct _DepInfo *info;
switch(message){
case MSG_INITDIALOG:
{
/* 将对话框参数lParam保存为窗口的附加数据,以备后用*/
info = (struct _DepInfo*)lParam;
SetWindowAdditionalData2 (hDlg, (DWORD)lParam);
break;
}
caseMSG_COMMAND:
{
/* 从窗口的附加数据中取出保存的对话框参数*/
info = (struct _DepInfo*) GetWindowAdditionalData2 (hDlg);
switch(wParam){
caseIDOK:
/* 使用info结构中的数据*/
......
caseIDCANCEL:
EndDialog(hDlg,wParam);
break;
}
}
}
returnDefaultDialogProc (hDlg, message, wParam, lParam);
}
通常而言,传递到对话框回调函数中的参数是一个结构的指针,该结构包含一些初始化对话框的数据,同时也可以将对话框的输入数据保存下来并传递到对话框之外使用。
6模态和非模态对话框
简单而言,模态对话框就是显示之后,用户不能再切换到其他主窗口进行工作的对话框,而只能在关闭之后,才能使用其他的主窗口。MiniGUI中,使用DialogBoxIndirectParam函数建立的对话框就是模态对话框。实际上,该对话框首先根据模板建立对话框,然后禁止其托管主窗口,并在主窗口的MSG_CREATE消息中创建控件,并发送MSG_INITDIALOG消息给回调函数,最终建立一个新的消息循环,并进入该消息循环,直到程序调用EndDialog函数为止。
实际上,我们也可以在MiniGUI中利用对话框模板建立普通的主窗口,即非模态对话框。这时,我们使用CreateMainWindowIndirect函数。该函数以及相关函数的原型如下(src/window.h):
1199HWND GUIAPI CreateMainWindowIndirect (PDLGTEMPLATEpDlgTemplate,
1200 HWND hOwner, WNDPROC WndProc);
1201 BOOL GUIAPIDestroyMainWindowIndirect (HWND hMainWin);
使用CreateMainWindowIndirect根据对话框模板建立的主窗口和其他类型的普通主窗口没有任何区别。
7小结
对话框编程是MiniGUI应用开发中使用最为常见的一种技术。通过定义一个对话框模板,就可以自动创建一个具有复杂输入输出界面的对话框。本文讲述了MiniGUI中的控件类和控件实例的关系,并举例说明控件子类化的概念及应用;然后讲解了MiniGUI对话框的编程技术,包括对话框模板的定义和对话框回调函数的编程;最后说明了模态对话框和非模态对话框之间的区别。
使用GDI函数
1引言
GUI 系统的一个重要组成部分就是GDI,即图形设备接口(GraphicsDevice Interface)。通过GDI,GUI程序就可以在计算机屏幕上,或者其他的显示设备上进行图形输出,包括基本绘图和文本输出。本文将详细描述MiniGUI中的GDI函数,并举例说明重要函数的用法。其中包括:DC的概念、获取和释放;矩形操作和剪切域操作;基本绘图函数;位图操作函数;逻辑字体操作函数等。
2图形设备上下文
在MiniGUI中,采用了在Windows和XWindow中普遍采用的图形设备概念。每个图形设备定义了计算机显示屏幕上的一个矩形输出区域。在调用图形输出函数时,均要求指定经初始化的图形设备上下文(DeviceContext,DC),也称作"设备环境"。从程序员的角度看,一个经过初始化的图形设备上下文确定了其后进行图形输出的一些基本属性,并一直保持这些属性,直到被改变为止。这些属性包括:输出的线条颜色、填充颜色、字体颜色、字体形状等等。而从GUI系统角度来讲,一个图形设备上下文所代表的含义就要复杂得多,它起码应该包含如下内容:
·该设备上下文本所在设备信息(显示模式、色彩深度、显存布局等等);
·该设备上下文所代表的窗口以及该窗口被其他窗口剪切的信息(在MiniGUI中,称作"全局剪切域");
·该设备上下文的基本操作函数(点、直线、多边形、填充、块操作等),及其上下文信息;
·由程序设定的局部信息(绘图属性、映射关系和局部剪切域等)。
所以,从程序员的角度看来,他所关心的仅仅是设备上下文本身的一小部分东西。
2.1设备上下文的获取和释放
在MiniGUI中,所有绘图相关的函数均需要有一个设备上下文。设备上下文可通过GetClientDC和ReleaseDC获取和释放。由GetDC所获取的设备上下文是针对整个窗口的,而GetClientDC所获取的设备上下文是针对窗口客户区,也就是说,前一个函数获得的设备上下文,其坐标原点位于窗口左上角,输出被限定在窗口范围之内;后一个函数获得的设备上下文,其坐标原点位于窗口客户区左上角,输出被限定在窗口客户区范围之内。下面是这三个函数的原型说明(include/gdi.h):
398HDC GUIAPI GetDC (HWND hwnd);
399 HDC GUIAPI GetClientDC (HWNDhwnd);
400 void GUIAPI ReleaseDC (HDC hdc);
GetDC和GetClientDC是从系统预留的若干个DC当中获得一个目前尚未使用的设备上下文。所以,应该注意如下两点:
在使用完成一个由GetDC返回的设备上下文之后,应该尽快调用ReleaseDC释放。
避免同时使用多个设备上下文,并避免在递归函数中调用GetDC和GetClientDC。
为了方便程序编写,提高绘图效率,MiniGUI还提供了建立私有设备上下文的函数,所建立的设备上下文在整个窗口生存期内有效,从而免除了获取和释放的过程。这些函数的原型如下:
403HDC GUIAPI CreatePrivateDC (HWND hwnd);
404 HDC GUIAPICreatePrivateClientDC (HWND hwnd);
405 HDC GUIAPIGetPrivateClientDC (HWND hwnd);
406 void GUIAPI DeletePrivateDC(HDC hdc);
在建立主窗口时,如果主窗口的扩展风格中指定了WS_EX_USEPRIVATEDC风格,则CreateMainWindow函数会自动为该窗口的客户区建立私有设备上下文。通过GetPrivateClientDC函数,可以获得该设备上下文。对控件而言,如果控件类具有CS_OWNDC属性,则所有属于该控件类的控件将自动建立私有设备上下文。DeletePrivateDC函数用来删除私有设备上下文。对上述两种情况,系统将在销毁窗口时自动调用DeletePrivateDC函数。
另外一个获取和释放设备上下文的方法是通过BeginPaint和EndPaint函数。这两个函数只能在处理MSG_PAINT的消息中调用。MiniGUI在BeginPaint函数中通过GetClientDC获取客户区设备上下文,然后将窗口当前的无效区域选择到窗口的剪切区域中;而EndPaint函数则清空窗口的无效区域,并释放设备上下文。这两个函数的原型如下(include/window.h):
623HDC GUIAPI BeginPaint(HWND hWnd);
624 void GUIAPI EndPaint(HWNDhWnd, HDC hdc);
因为BeginPaint函数将窗口的无效区域选择到了设备上下文中,所以,可以通过一些必要的优化来提高MSG_PAINT消息的处理效率。比如,某个程序要在窗口客户区中填充若干矩形,就可以在MSG_PAINT函数中如下处理:
MSG_PAINT:
{
HDC hdc = BeginPaint (hWnd);
for(j = 0; j < 10; j ++) {
if (RectVisible (hdc, rcs + j)) {
FillBox (hdc, rcs[j].left, rcs[j].top, rcs [j].right, rcs[j].bottom);
}
}
EndPaint(hWnd, hdc);
return 0;
}
这样可以避免不必要的重绘操作,从而提高绘图效率。
2.2系统内存中的设备上下文
MiniGUI也提供了内存设备上下文的创建和销毁函数。利用内存设备上下文,可以在系统内存中建立一个类似显示内存的区域,然后在该区域中进行绘图操作,结束后再复制到显示内存中。这种绘图方法有许多好处,比如速度很快,减少直接操作显存造成的闪烁现象等等。不过,目前MiniGUI中只能建立和显示内存,也就是物理设备上下文一样的内存设备上下文。用来建立和销毁内存设备上下文的函数原型如下(include/gdi.h):
401HDC GUIAPI CreateCompatibleDC (HDC hdc);
402 void GUIAPIDeleteCompatibleDC (HDC hdc);
2.3屏幕设备上下文
MiniGUI 在启动之后,就建立了一个全局的屏幕设备上下文。该DC是针对整个屏幕的,并且没有任何预先定义的剪切域。在某些应用程序中,可以直接使用该设备上下文进行绘图,将大大提高绘图效率。在MiniGUI中,屏幕设备上下文用HDC_SCREEN标识,不需要进行任何获取和释放操作。
2.4映射模式
一个设备上下文被初始化之后,其坐标系原点通常是输出矩形的左上角,而x轴水平向左,y轴垂直向下,并以象素为单位。这种坐标的映射模式标识为MM_TEXT。MiniGUI提供了一套函数,可以改变这种映射方式,包括对默认坐标系进行偏移、缩放等操作。这些函数的原型如下(include/gdi.h):
453int GUIAPI GetMapMode (HDC hdc);
454 void GUIAPI GetViewportExt(HDC hdc, POINT* pPt);
455 void GUIAPI GetViewportOrg (HDC hdc,POINT* pPt);
456 void GUIAPI GetWindowExt (HDC hdc, POINT*pPt);
457 void GUIAPI GetWindowOrg (HDC hdc, POINT* pPt);
458void GUIAPI SetMapMode (HDC hdc, int mapmode);
459 void GUIAPISetViewportExt (HDC hdc, POINT* pPt);
460 void GUIAPISetViewportOrg (HDC hdc, POINT* pPt);
461 void GUIAPI SetWindowExt(HDC hdc, POINT* pPt);
462 void GUIAPI SetWindowOrg (HDC hdc,POINT* pPt);
GetMapMode函数返回当前的映射模式,若不是MM_TEXT模式,则返回MM_ANISOTROPIC。SetMapMode函数设置映射模式,MiniGUI目前只支持两种映射模式,即MM_ANISOTROPIC和MM_TEXT。Get函数组用来返回映射模式信息,包括偏移量、缩放比例等等,而Set函数组用来设置相应的映射信息。
通常情况下,MiniGUI的GDI函数所指定的坐标参数称为"逻辑坐标",在绘制之前,首先要转化成"设备坐标"。当使用MM_TEXT映射模式时,逻辑坐标和设备坐标是等价的。LPtoDP函数用来完成逻辑坐标到设备坐标的转换,DPtoLP函数用来完成从设备坐标到逻辑坐标的转换。逻辑坐标和设备坐标的关系可从LPtoDP函数中看到(src/gdi/coor.c):
61void GUIAPI LPtoDP(HDC hdc, POINT* pPt)
62 {
63 PDC pdc;
64
65 pdc = dc_HDC2PDC(hdc);
66
67 if (pdc->mapmode != MM_TEXT) {
68 pPt->x = (pPt->x - pdc->WindowOrig.x)
69 * pdc->ViewExtent.x / pdc->WindowExtent.x
70 + pdc->ViewOrig.x;
71
72 pPt->y = (pPt->y - pdc->WindowOrig.y)
73 * pdc->ViewExtent.y / pdc->WindowExtent.y
74 + pdc->ViewOrig.y;
75 }
76 }
77
另外,LPtoSP函数和SPtoLP函数完成逻辑坐标和屏幕坐标之间的转换。
3矩形操作和区域操作
3.1矩形操作
在 MiniGUI中,矩形是如下定义的(include/common.h):
120typedef struct tagRECT
121 {
122 intleft;
123 int top;
124 int right;
125 int bottom;
126 }RECT;
127 typedef RECT* PRECT;
128 typedef RECT* LPRECT;
简而言之,矩形就是用来表示屏幕上一个矩形区域的数据结构,定义了矩形左上角的x,y 坐标(left和top)以及右下角的x,y 坐标(right和bottom)。需要注意的是,MiniGUI中的矩形,其右侧的边和下面的边是不属于该矩形的。例如,要表示屏幕上的一条扫描线,应该用
RECTrc = {x, y, x + w + 1, y + 1};
表示。其中x是扫描线的起点,y是扫描线的垂直位置,w是扫描线宽度。
MiniGUI提供了一组函数,可对RECT对象进行操作:
SetRect对RECT对象的各个分量进行赋值;
SetRectEmpty将RECT对象设置为空。MiniGUI中的空矩形定义为高度或宽度为零的矩形;
IsRectEmpty判断给定RECT对象是否为空。
NormalizeRect对给定矩形进行正规化处理。MiniGUI中的矩形,应该满足(right> left 并且bottom> top)的条件。满足这一条件的矩形又称"正规化矩形",该函数可以对任意矩形进行正规化处理。
CopyRect复制矩形;
EqualRect判断两个RECT对象是否相等,即两个RECT对象的各个分量相等;
IntersectRect该函数求两个RECT对象之交集。若两个矩形根本不相交,则函数返回FALSE,且结果矩形未定义;否则返回交矩形。
DoesIntersec该函数仅仅判断两个矩形是否相交。
IsCovered该函数判断RECT对象A是否全部覆盖RECT对象B,即RECTB 是RECTA 的真子集。
UnionRect该函数求两个矩形之并。如果两个矩形根本无法相并,则返回FALSE。两个相并之后的矩形,其中所包含的任意点,应该属于两个相并矩形之一。
GetBoundRect该函数求两个矩形的外包最小矩形。
SubstractRect该函数从一个矩形中减去另外一个矩形。注意,两个矩形相减的结果可能生成4个不相交的矩形。该函数将返回结果矩形的个数以及差矩形。详细信息可参见"MiniGUI体系结构之二――多窗口管理和控件及控件类"一文。
OffsetRect该函数对给定的RECT对象进行平移处理。
InflateRect该函数对给定的RECT对象进行膨胀处理。注意膨胀之后的矩形宽度和高度是给定膨胀值的两倍。
InflateRectToPt该函数将给定的RECT对象膨胀到指定的点。
PtInRect该函数判断给定的点是否位于指定的RECT对象中。
3.2区域操作
在MiniGUI中,区域定义为互不相交矩形的集合,在内部用链表形式表示。MiniGUI的区域可以用来表示窗口的剪切域、无效区域、可见区域等等。在MiniGUI中,区域和剪切域的定义是一样的,剪切域定义如下(include/gdi.h):
76// Clip Rect
77 typedef struct tagCLIPRECT
78 {
79 RECT rc;
80 struct tagCLIPRECT* next;
81 }CLIPRECT;
82typedef CLIPRECT* PCLIPRECT;
83
84 // ClipRegion
85 typedef struct tagCLIPRGN
86 {
87 RECT rcBound; // bound rect of clip region
88 PCLIPRECT head; // clip rect listhead
89 PCLIPRECT tail; // clip rect listtail
90 PBLOCKHEAP heap; // heap of cliprect
91 } CLIPRGN;
92 typedef CLIPRGN* PCLIPRGN;
每个剪切域对象有一个BLOCKHEAP成员。该成员是剪切域分配RECT对象的私有堆。在使用一个剪切域对象之前,首先应该建立一个BLOCKHEAP对象,并对剪切域对象进行初始化。如下所示:
staticBLOCKHEAP sg_MyFreeClipRectList;
...
CLIPRGNmy_region
InitFreeClipRectList(&sg_MyFreeClipRectList, 20);
InitClipRgn(&my_regioni, &sg_MyFreeClipRectList);
在实际使用当中,多个剪切域可以共享同一个BLOCKHEAP对象。
在初始化剪切域对象之后,可以对剪切域进行如下操作:
SetClipRgn该函数将剪切域设置为仅包含一个矩形的剪切域;
ClipRgnCopy该函数复制剪切域;
ClipRgnIntersect该函数求两个剪切域的交集;
GetClipRgnBoundRect该函数求剪切域的外包最小矩形;
IsEmptyClipRgn该函数判断剪切域是否为空,即是否包含剪切矩形;
EmptyClipRgn该函数释放剪切域中的剪切矩形,并清空剪切域;
AddClipRect该函数将一个剪切矩形追加到剪切域中。注意该操作并不判断该剪切域是否和剪切矩形相交。
IntersectClipRect该函数求剪切区域和给定矩形相交的剪切区域。
SubtractClipRect该函数从剪切区域中减去指定的矩形。
矩形和区域的运算构成了窗口管理的主要算法,也是高级GDI函数的基本算法之一,在GUI编程中占有非常重要的地位。
4基本图形操作
4.1基本绘图属性
在了解基本绘图函数之前,我们首先了解一下基本绘图属性。在MiniGUI的目前版本中,绘图属性比较少,大体包括线条颜色、填充颜色、文本背景模式、文本颜色、TAB键宽度等等。表1给出了这些属性的操作函数。
表1基本绘图属性及其操作函数
绘图属性操作函数 受影响的 GDI函数
线条颜色GetPenColor/SetPenColorLineTo、Circle、Rectangle
填充颜色GetBrushColor/SetBrushColorFillBox
文本背景模式GetBkMode/SetBkModeTextOut、DrawText
文本颜色GetTextColor/SetTextColor同上
TAB键宽度GetTabStop/SetTabStop同上
MiniGUI目前版本中还定义了刷子和笔的若干函数,这些函数是为将来兼容性而定义的,目前无用。
4.2基本绘图函数
MiniGUI 中的基本绘图函数为点、线、圆、矩形、调色板操作等基本函数,原型定义如下(include/gdi.h,版本1.0.06):
433// Palette support
434 int GUIAPI GetPalette (HDC hdc, int start,int len, gal_color* cmap);
435 int GUIAPI SetPalette (HDC hdc, intstart, int len, gal_color* cmap);
436 int GUIAPISetColorfulPalette (HDC hdc);
437
438 // General drawingsupport
439 void GUIAPI SetPixel (HDC hdc, int x, int y, gal_pixelc);
440 void GUIAPI SetPixelRGB (HDC hdc, int x, int y, int r, intg, int b);
441 gal_pixel GUIAPI GetPixel (HDC hdc, int x, inty);
442 void GUIAPI GetPixelRGB (HDC hdc, int x, int y, int* r,int* g, int* b);
443 gal_pixel GUIAPI RGB2Pixel (HDC hdc, int r,int g, int b);
444
445 void GUIAPI LineTo (HDC hdc, int x, inty);
446 void GUIAPI MoveTo (HDC hdc, int x, int y);
447
448void GUIAPI Circle (HDC hdc, int x, int y, int r);
449 void GUIAPIRectangle (HDC hdc, int x0, int y0, int x1, int y1);
这里有两个基本的概念需要明确区分,即象素值和RGB值。RGB是计算机中通过三原色的不同比例表示某种颜色的方法。通常,RGB中的红、绿、蓝可取0~ 255 当中的任意值,从而可以表示255x255x255种不同的颜色。而在显示内存当中,要显示在屏幕上的颜色并不是用RGB这种方式表示的,显存当中保存的其实是所有象素的象素值。象素值的范围根据显示模式的不同而变化。在16色显示模式下,象素值范围为[0,15];而在256色模式下,象素值范围为[0,255];在16位色模式下,象素值范围为[0,2^16 - 1]。通常我们所说显示模式是多少位色,就是指象素的位数。
在 MiniGUI中,设置某个象素点的颜色,既可以直接使用象素值(SetPixel),也可以间接通过RGB值来设置(SetPixelRGB),并且通过RGB2Pixel函数,可以将RGB值转换为象素值。
调色板是低颜色位数的模式下(比如256色或者更少的颜色模式),用来建立有限的象素值和RGB对应关系的一个线性表。在MiniGUI当中,可以通过SetPalette和GetPalette进行调色板的操作,而SetColorfulePalette将调色板设置为默认的调色板。一般而言,在更高的颜色位数,比如15位色以上,因为象素值范围能够表达的颜色已经非常丰富了,加上存储的关系,就不再使用调色板建立象素值和RGB的对应关系,而使用更简单的方法建立RGB和实际象素之间的关系,如下所示(src/gal/native/native.h):
174/* Truecolor color conversion and extraction macros */
175 /*
176 * Conversion from RGB to gal_pixel
177 */
178 /* create24 bit 8/8/8 format pixel (0x00RRGGBB) from RGB triplet*/
179#define RGB2PIXEL888(r,g,b) \
180 (((r) << 16) | ((g) << 8) | (b))
181
182 /* create16 bit 5/6/5 format pixel from RGB triplet */
183 #defineRGB2PIXEL565(r,g,b) \
184 ((((r) & 0xf8) << 8) | (((g) & 0xfc) << 3) |(((b) & 0xf8) >> 3))
185
186 /* create 15 bit 5/5/5format pixel from RGB triplet */
187 #defineRGB2PIXEL555(r,g,b) \
188 ((((r) & 0xf8) << 7) | (((g) & 0xf8) << 2) |(((b) & 0xf8) >> 3))
189
190 /* create 8 bit 3/3/2format pixel from RGB triplet*/
191 #defineRGB2PIXEL332(r,g,b) \
192 (((r) & 0xe0) | (((g) & 0xe0) >> 3) | (((b) & 0xc0)>> 6))
RGB2PIXEL888将[0,255] 的RGB值转换为24位色的象素值;而RGB2PIXEL565转换为16位色的象素值;RGB2PIXEL555和RGB2PIXEL332分别转换为15位色和8位色。
4.3剪切域操作函数
在利用设备上下文进行绘图时,还可以进行剪切处理。MiniGUI提供了如下函数完成对指定设备上下文的剪切处理(include/gdi.h):
468// Clipping support
469 void GUIAPI ExcludeClipRect (HDC hdc, intleft, int top,
470 int right, int bottom);
471 void GUIAPI IncludeClipRect (HDC hdc,int left, int top,
472 int right, int bottom);
473 void GUIAPI ClipRectIntersect (HDChdc, const RECT* prc);
474 void GUIAPI SelectClipRect (HDC hdc,const RECT* prc);
475 void GUIAPI SelectClipRegion (HDC hdc, constCLIPRGN* pRgn);
476 void GUIAPI GetBoundsRect (HDC hdc, RECT*pRect);
477 BOOL GUIAPI PtVisible (HDC hdc, const POINT* pPt);
478BOOL GUIAPI RectVisible (HDC hdc, const RECT* pRect);
ExcludeClipRect从设备上下文的当前可见区域中排除给定的矩形区域,设备上下文的可见区域将缩小;IncludeClipRect向当前设备上下文的可见区域中添加一个矩形区域,设备上下文的可见区域将扩大;ClipRectIntersect将设备上下文的可见区域设置为已有区域和给定矩形区域的交集;SelectClipRect将设备上下文的可见区域重置为一个矩形区域;SelectClipRegion将设备上下文的可见区域设置为一个指定的区域;GetBoundsRect获取当前可见区域的外包最小矩形;PtVisible和RectVisible用来判断给定的点或者矩形是否可见,即是否全部或部分落在可见区域当中。
5位图操作函数
在MiniGUI的GDI函数中,位图操作函数占有非常重要的地位。实际上,许多高级绘图操作函数均建立在位图操作函数之上,比如文本输出函数。MiniGUI的主要位图操作函数如下所示(include/gdi.h):
495void GUIAPI FillBox (HDC hdc, int x, int y, int w, int h);
496void GUIAPI FillBoxWithBitmap (HDC hdc, int x, int y, int w, inth,
497 PBITMAP pBitmap);
498 void GUIAPI FillBoxWithBitmapPart (HDC hdc,int x, int y, int w, int h,
499 int bw, int bh, PBITMAP pBitmap, int xo, int yo);
500
501 voidGUIAPI BitBlt (HDC hsdc, int sx, int sy, int sw, intsh,
502 HDC hddc, int dx, int dy, DWORD dwRop);
503 void GUIAPI StretchBlt(HDC hsdc, int sx, int sy, int sw, int sh,
504 HDC hddc, int dx, int dy, int dw, int dh, DWORD dwRop);
FillBox用当前填充色填充矩形框;FillBoxWithBitmap用设备相关位图对象填充矩形框,可以用来扩大或者缩小位图;FillBoxWithBitmapPart用设备相关位图对象的部分填充矩形框,也可以扩大或缩小位图。BitBlt函数用来实现两个不同设备上下文之间的显示内存复制。StretchBlt则在BitBlt的基础上进行缩放操作。
通过MiniGUI的LoadBitmap函数,可以将某种位图文件装载为MiniGUI设备相关的位图对象,即BITMAP对象。设备相关的位图指的是,位图当中包含的是与指定设备上下文的显示模式相匹配的象素值,而不是设备无关的位图信息。MiniGUI目前可以用来装载BMP文件、JPG文件、GIF文件以及PCX、TGA等格式的位图文件,而LoadMyBitmap函数则用来将位图文件装载成设备无关的位图对象。在MiniGUI中,设备相关的位图对象和设备无关的位图对象分别用BITMAP和MYBITMAP两种数据结构表示。相关函数的原型如下(include/gdi.h):
666int GUIAPI LoadMyBitmap (HDC hdc, PMYBITMAP pMyBitmap, RGB* pal,const char* spFileName);
667 int GUIAPI LoadBitmap (HDC hdc,PBITMAP pBitmap, const char* spFileName);
668 #ifdef_SAVE_BITMAP
669 int GUIAPI SaveBitmap (HDC hdc, PBITMAP pBitmap,const char* spFileName);
670 #endif
671 void GUIAPIUnloadBitmap (PBITMAP pBitmap);
672
673 int GUIAPIExpandMyBitmap (HDC hdc, const MYBITMAP* pMyBitmap, const RGB* pal,PBITMAP pBitmap);
674
675 void GUIAPI ExpandMonoBitmap (HDChdc, int w, int h, const BYTE* bits, int bits_flow, intpitch,
676 BYTE* bitmap, int bg, int fg);
677 void GUIAPI Expand16CBitmap(HDC hdc, int w, int h, const BYTE* bits, int bits_flow, intpitch,
678 BYTE* bitmap, const RGB* pal);
679 void GUIAPI Expand256CBitmap(HDC hdc, int w, int h, const BYTE* bits, int bits_flow, intpitch,
680 BYTE* bitmap, const RGB* pal);
681 void GUIAPI CompileRGBBitmap(HDC hdc, int w, int h, const BYTE* bits, int bits_flow, intpitch,
682 BYTE* bitmap, int rgb_order);
683
684 void GUIAPIReplaceBitmapColor (HDC hdc, PBITMAP pBitmap, int iOColor, intiNColor);
上面的Expand函数组,用来将设备无关的位图转化为与指定设备上下文相关的位图对象。
有关位图操作的详细使用方法,可见mglite-exec包中的bitmaptest示例程序。
6逻辑字体和文本输出函数
MiniGUI的逻辑字体功能强大,它包括了字符集、字体类型、风格、样式等等丰富的信息,不仅仅可以用来输出文本,而且可以用来分析多语种文本的结构。这在许多文本排版应用中非常有用。在使用MiniGUI的逻辑字体之前,首先要创建逻辑字体,并且将其选择到要使用这种逻辑字体进行文本输出的设备上下文当中。每个设备上下文的默认逻辑字体是系统字体,即用来显示菜单、标题的逻辑字体。你可以调用CreateLogFont和CreateLogFontIndirect两个函数来建立逻辑字体,并利用SelectFont函数将逻辑字体选择到指定的设备上下文中,在使用结束之后,用DestroyLogFont函数销毁逻辑字体。注意你不能销毁正被选中的逻辑字体。这几个函数的原型如下(include/gdi.h):
555PLOGFONT GUIAPI CreateLogFont (const char* type, const char*family,
556 constchar* charset, char weight, char slant, char set_width,
557 char spacing, char underline, char struckout,
558 int size, int rotation);
559 PLOGFONT GUIAPI CreateLogFontIndirect(LOGFONT* logfont);
560 void GUIAPI DestroyLogFont (PLOGFONTlog_font);
561
562 void GUIAPI GetLogFontInfo (HDC hdc,LOGFONT* log_font);
563
564 #define SYSLOGFONT_DEFAULT 0
565 PLOGFONT GUIAPI GetSystemFont (int font_id);
566
567PLOGFONT GUIAPI GetCurFont (HDC hdc);
568 PLOGFONT GUIAPISelectFont (HDC hdc, PLOGFONT log_font);
GetSystemFont函数返回默认的系统逻辑字体,GetCurFont函数返回当前选中的逻辑字体。注意不要删除系统逻辑字体。下面的程序段建立了多个逻辑字体:
staticLOGFONT *logfont, *logfontgb12, *logfontbig24;
logfont= CreateLogFont (NULL, "SansSerif","ISO8859-1",
FONT_WEIGHT_REGULAR, FONT_SLANT_ITALIC,FONT_SETWIDTH_NORMAL,
FONT_SPACING_CHARCELL, FONT_UNDERLINE_NONE,FONT_STRUCKOUT_LINE,
16, 0);
logfontgb12 = CreateLogFont (NULL, "song","GB2312",
FONT_WEIGHT_REGULAR, FONT_SLANT_ROMAN,FONT_SETWIDTH_NORMAL,
FONT_SPACING_CHARCELL, FONT_UNDERLINE_LINE,FONT_STRUCKOUT_LINE,
12, 0);
logfontbig24 = CreateLogFont (NULL, "ming","BIG5",
FONT_WEIGHT_REGULAR, FONT_SLANT_ROMAN,FONT_SETWIDTH_NORMAL,
FONT_SPACING_CHARCELL, FONT_UNDERLINE_LINE,FONT_STRUCKOUT_NONE,
24, 0);
其中,第一个字体,即logfont是属于字符集ISO8859-1的字体,并且选用SansSerif体,大小为16象素高;logfontgb12是属于字符集GB2312的字体,并选用song体(宋体),大小为12象素高;logfontbig24是属于字符集BIG5的字体,并选用ming体(即明体)。
在建立了逻辑字体之后,应用程序可以利用逻辑字体进行多语种混和文本的分析。这里的多语种混和文本是指,两个不相交字符集的文本组成的字符串,比如GB2312和ISO8859-1,或者BIG5和ISO8859-2,通常是多字符集和单字符集之间的混和。利用下面的函数,可以实现多语种混和文本的文本组成分析(include/gdi.h):
570// Text parse support
571 int GUIAPI GetTextMCharInfo (PLOGFONTlog_font, const char* mstr, int len,
572 int* pos_chars);
573 int GUIAPI GetTextWordInfo (PLOGFONTlog_font, const char* mstr, int len,
574 int* pos_words, WORDINFO* info_words);
575 int GUIAPIGetFirstMCharLen (PLOGFONT log_font, const char* mstr, int len);
576int GUIAPI GetFirstWord (PLOGFONT log_font, const char* mstr, intlen,
577 WORDINFO* word_info);
GetTextMCharInfo函数返回多语种混和文本中每个字符的字节位置。比如对"ABC汉语"字符串,该函数将在pos_chars中返回{0,1, 2, 3, 5} 5 个值。GetTextWordInfo函数则将分析多语种混和文本中每个单词的位置。对单字节字符集文本,单词以空格、TAB键为分界,对多字节字符集文本,单词以单个字符为界。GetFirstMCharLen函数返回第一个混和文本字符的字节长度。GetFirstWord函数返回第一个混和文本单词的单词信息。
以下函数可以用来计算逻辑字体的输出长度和高度信息(include/gdi.h):
580int GUIAPI GetTextExtentPoint (HDC hdc, const char* text, int len,int max_extent,
581 int* fit_chars, int* pos_chars, int* dx_chars, SIZE* size);
582
583// Text output support
584 int GUIAPI GetFontHeight (HDC hdc);
585int GUIAPI GetMaxFontWidth (HDC hdc);
586 void GUIAPIGetTextExtent (HDC hdc, const char* spText, int len, SIZE*pSize);
587 void GUIAPI GetTabbedTextExtent (HDC hdc, const char*spText, int len, SIZE* pSize);
GetTextExtentPoint函数计算在给定的输出宽度内输出多字节文本时(即输出的字符限制在一定的宽度当中),可输出的最大字符个数、每个字符所在的字节位置、每个字符的输出位置,以及实际的输出高度和宽度。GetFontHeight和GetMaxFontWidth则返回逻辑字体的高度和最大字符宽度。GetTextExtent计算文本的输出高度和宽度。GetTabbedTextExtent函数返回格式化字符串的输出高度和宽度。
以下函数用来输出文本(include/gdi.h):
596int GUIAPI TextOutLen (HDC hdc, int x, int y, const char* spText, intlen);
597 int GUIAPI TabbedTextOutLen (HDC hdc, int x, int y,const char* spText, int len);
598 int GUIAPI TabbedTextOutEx (HDChdc, int x, int y, const char* spText, int nCount,
599 int nTabPositions, int *pTabPositions, int nTabOrigin);
600 voidGUIAPI GetLastTextOutPos (HDC hdc, POINT* pt);
601
602 //Compatiblity definitions
603 #define TextOut(hdc, x, y, text) TextOutLen (hdc, x, y, text, -1)
604 #define TabbedTextOut(hdc, x,y, text) TabbedTextOutLen (hdc, x, y, text, -1)
...
621int GUIAPI DrawTextEx (HDC hdc, const char* pText, intnCount,
622 RECT* pRect, int nIndent, UINT nFormat);
TextOutLen函数用来在给定位置输出指定长度的字符串,若长度为-1,则字符串必须是以'\0'结尾的。TabbedTextOutLen函数用来输出格式化字符串。TabbedTextOutEx函数用来输出格式化字符串,但可以指定字符串中每个TAB键的位置。DrawText是功能最复杂的输出函数,可以以不同的对齐方式在指定矩形内部输出文本。下面的程序段,就根据字符串所描述的那样,调用DrawText函数进行对齐文本输出:
voidOnModeDrawText (HDC hdc)
{
RECT rc1, rc2,rc3, rc4;
const char* szBuff1 = "This is agood day. \n"
"这是利用DrawText绘制的文本,使用字体GB2312Song 12. "
"文本垂直靠上,水平居中";
const char* szBuff2 = "This is a good day. \n"
"这是利用DrawText绘制的文本,使用字体GB2312Song 16. "
"文本垂直靠上,水平靠右";
const char* szBuff3 = "单行文本垂直居中,水平居中";
const char* szBuff4 =
"这是利用DrawTextEx绘制的文本,使用字体GB2312Song 16. "
"首行缩进值为32.文本垂直靠上,水平靠左";
rc1.left= 1; rc1.top = 1; rc1.right = 401; rc1.bottom = 101;
rc2.left = 0; rc2.top = 110; rc2.right = 401; rc2.bottom =351;
rc3.left = 0; rc3.top = 361;rc3.right = 401; rc3.bottom = 451;
rc4.left =0; rc4.top = 461; rc4.right = 401; rc4.bottom = 551;
SetBkColor(hdc, COLOR_lightwhite);
Rectangle(hdc, rc1.left, rc1.top, rc1.right, rc1.bottom);
Rectangle (hdc, rc2.left, rc2.top, rc2.right, rc2.bottom);
Rectangle (hdc, rc3.left, rc3.top, rc3.right, rc3.bottom);
Rectangle (hdc, rc4.left, rc4.top, rc4.right, rc4.bottom);
InflateRect(&rc1, -1, -1);
InflateRect (&rc2, -1,-1);
InflateRect (&rc3, -1, -1);
InflateRect (&rc4, -1, -1);
SelectFont(hdc, logfontgb12);
DrawText (hdc, szBuff1, -1,&rc1, DT_NOCLIP | DT_CENTER | DT_WORDBREAK);
SelectFont(hdc, logfontgb16);
DrawText (hdc, szBuff2, -1,&rc2, DT_NOCLIP | DT_RIGHT | DT_WORDBREAK);
SelectFont(hdc, logfontgb24);
DrawText (hdc, szBuff3, -1,&rc3, DT_NOCLIP | DT_SINGLELINE | DT_CENTER | DT_VCENTER);
SelectFont(hdc, logfontgb16);
DrawTextEx (hdc, szBuff4,-1, &rc4, 32, DT_NOCLIP | DT_LEFT | DT_WORDBREAK);
}
有关逻辑字体和文本输出的函数详细使用方法,可见mglite-exec包中的fontest示例程序。
7小结
本文讲述了MiniGUI中接口最多也最复杂的GDI函数及其使用方法。其中包括:设备上下文的概念、获取和释放;矩形操作和区域操作;基本绘图函数;位图操作函数;逻辑字体操作函数等等。目前版本的GDI接口还有许多功能上的缺陷,我们将在下一个版本开发中着重进行改善。关于MiniGUI下一版本的开发计划,请参见本文附录。
附录:MiniGUI的最新开发计划
MiniGUI 发展到今天,得到了许多用户的认可,使用它的人也越来越多了。目前,用户已经从国内发展到了国外。这说明MiniGUI当中的许多设计思想得到了认可,也大大激励了我们的开发热情。
作为一个面向实时嵌入式系统的GUI,MiniGUI的1.0.xx版本基本能够满足许多嵌入式系统的应用需求。但这还远远不够,我们仍然需要进一步的开发,以便让MiniGUI在嵌入式GUI系统中达到领先地位。
MiniGUI发展到今天,得到了许多用户的认可,使用它的人也越来越多了。目前,用户已经从国内发展到了国外。这说明MiniGUI当中的许多设计思想得到了认可,也大大激励了我们的开发热情。
作为一个面向实时嵌入式系统的GUI,MiniGUI的1.0.xx版本基本能够满足许多嵌入式系统的应用需求。但这还远远不够,我们仍然需要进一步的开发,以便让MiniGUI在嵌入式GUI系统中达到领先地位。
我们已经开始了MiniGUI新版本开发(即1.1.xx),对这个版本,有如下新的设想:
MiniGUI-Lite的全局鼠标支持。目前的MiniGUI-Lite版本,鼠标的位置刷新是由鼠标所在客户或者服务器管理的。新版本中,将考虑由服务器统一管理。这个工作目前已经基本完成。
在MiniGUI-Lite中添加层(Layer)的概念和处理。在一次MiniGUI-Lite会话中,可以建立多个层。每个层中可以包含能够同时向屏幕输出的多个客户,而每一时刻,能够在屏幕上显示的层只有一个。对层而言,我们可以进行层的激活处理。激活的层,将显示在屏幕上,而其他层的绘图将被屏蔽。对层中客户的绘图屏蔽算法,将考虑使用不同于当前MiniGUI-Lite通过信号和信号量结合的方法,因为这种方法在多线程应用中,可能出现问题。
层中客户可以互相剪切。后建立的客户,将剪切先建立的客户矩形。为此,要为每个层建立一个共享内存的IPC对象,客户通过该对象访问当前层客户之间的重叠和覆盖情况,而且要建立一个面向层的信号量和age值,用来协调客户剪切矩形的变化。层的客户剪切矩形的变化,将影响各个客户所建立窗口的全局剪切区域,从而影响DC的可见区域。
一个层中客户之间形成的Z序是固定的。不过,如果按照3所描述的方法,其实Z序也是可以变化的。考虑到性能因素,客户在层中所占的显示矩形不能变化,也就是说,既不能改变大小,也不能移动。但能够改变Z序,即改变客户之间的互相层叠关系。
BTW:为什么要如此考虑?
通过上面的方法,可以将一组具有共同目标的客户放置在同一个层上。比如,层中可以有一个vcongui程序,用它可以调试其他的MiniGUI程序。再比如,在VOD等程序中,实时播放VCD的客户就可以嵌入到主控界面当中。而服务器将具有较少的GUI能力,仅仅提供一个任务栏,用来激活某个层,或者改变客户在一个层中的Z序。
当然,这样安排对某些小屏幕的嵌入式应用来讲,比如PDA,并不是非常适合,但对STB、或者其他具有大屏幕的实时嵌入式系统来讲,将具有非常高的应用价值。
底层图形引擎将进行非常重大的修改,这将影响到MiniGUI-Threads和MiniGUI-Lite两个版本。目前的MiniGUI图形引擎,因为受到历史原因的影响,有许多弊端。在新的版本中,我们将考虑类似SDL那样的设计方法,将底层图形设备抽象为一个内存对象,并考虑加速功能的实现。同时,我们还要实现许多尚未实现的图形功能,包括光栅操作、Alpha混和、多边形支持、椭圆和弧线支持等等。
BTW:当前GAL的设计弊端
当前GAL的设计弊端主要是抽象层次太高,而且并没有在底层实现剪切域的直接支持。这是要在新版本中着重考虑改进的。新的剪切域算法,将考虑生成x-y-banned的剪切域,以便底层绘图函数能够直接利用剪切域进行设计绘图算法。
将考虑在MiniGUI-Lite版本中实现对矢量字体的支持,同时增加Cache处理能力,以便提高矢量字体的渲染效率。
对窗口管理,在这次开发中将不作大的修改,主要将进行一些代码的清理工作。
以上是我们对新版本的一些想法,希望大家能够讨论,并请多提建议和意见。我们将考虑首先实现层,然后实现图形引擎的改进,最后实现矢量字体在MiniGUI-Lite当中的支持及优化。如果您对MiniGUI新版本的开发有兴趣,可以加入我们的邮件列表,详细信息请参见http://www.minigui.org/ctalk.shtml。
MiniGUI1.1.0引入的新GDI功能和函数(1)
1引言
在本系列开发指南(四)中,我们详细讲解了MiniGUI的GDI函数及其使用。我们也曾提到,MiniGUI现有的GDI函数和功能,尚不能对机顶盒、瘦客户机等高端嵌入式系统提供良好支持。因此,我们在MiniGUI1.1.0 版本的开发中,重点对GAL和GDI进行了大规模的改良,几乎重新编写了所有代码。这些新的接口和功能,首先出现在最近发布的MiniGUI1.1.0Pre4 版本中,为了帮助开发人员正确理解和使用这些功能,特撰文说明新GAL和新GDI的接口和功能。
2新GAL和新GDI接口的设计目标
首先,MiniGUI旧的GDI接口非常简单,其功能主要集中在位图和块操作上,例如FillBox函数、FillBoxWithBitmap等等,而缺少对其他高级图形操作的支持,比如椭圆、圆弧、样条曲线等等。其次,旧的GDI接口还缺少基本的光栅操作功能。这里的光栅操作,指欲设定象素如何与屏幕上已有的象素进行运算。最基本的光栅操作功能是二进制的位操作,包括与、或、异或以及直接设置等等;高级的光栅操作包括透明处理和Alpha混和。这些GDI功能的缺乏,使得MiniGUI在机顶盒、瘦客户等系统中应用时,显得"力不从心"。再次,旧的GDI接口基本上没有考虑到任何硬件加速功能。我们大家都知道,显示卡自身提供的硬件加速功能,能够大大提高图形程序的运行速度,使得画面流畅而自然,如果GUI系统不能充分利用这些硬件特性的话,则图形处理能力将大打折扣。最后,旧的GAL设计存在抽象层次太高的问题,导致了GAL引擎臃肿,且重复代码很多,也不便于进行代码上的优化。
综上所述,我们参照著名的跨平台游戏和多媒体函数库SDL(SimpleDirectMedia Layer)对GAL引擎结构进行了重新设计,并且重新实现了所有的GDI函数,使得新的GDI接口具备如下特性:
·能够充分利用硬件特性,包括显示内存和硬件加速能力。
·支持高级图形操作,包括基本光栅操作、透明处理和Alpha混和。
·增强了剪切区域处理能力,有助于优化图形输出函数。
·增强了原有的BITMAP接口,使之支持透明和Alpha通道。
·充分利用嵌入式汇编代码进行代码优化。
下面将重点讲述新的GAL功能和新的GDI接口。
3新GAL功能特性
新的GAL结构来自著名的跨平台游戏和多媒体库SDL(SimpleDirectMedia Layer)。目前提供了对LinuxFrameBuffer 的支持,计划在将来提供对X、SVGALib和VGL(FreeBSD)等等图形库的支持。
3.1GAL 和GDI的关系
大家都知道,MiniGUI的GAL是一个图形抽象层,提供给上层GDI函数一些基础的功能和设施。在先前的设计中,GAL可以看成是GDI图形驱动程序,许多图形操作函数,比如点、线、矩形填充、位图操作等等,均通过GAL的相应函数完成。这种设计的最大问题是无法对GDI进行扩展。比如要增加椭圆绘制函数,就需要在每个引擎当中实现椭圆的绘制函数。并且GDI管理的是剪切域,而GAL引擎却基于剪切矩形进行操作。这种方法也导致了GDI函数无法进行绘制优化。因此,在新的GAL和GDI接口设计中,我们将GAL的接口进行了限制,而将原有许多由GAL引擎完成的图形输出函数,提高到上层GDI函数中完成。GAL和GDI的新的功能划分如下:
·GAL负责对显示设备进行初始化,并管理显示内存的使用;
·GAL 负责为上层GDI提供映射到进程地址空间的线性显示内存,以及诸如调色板等其他相关信息;
·GAL 负责实现快速的位块操作,包括矩形填充和Blitting操作等,并且在可能的情况下,充分利用硬件加速功能;
·GDI 函数实现高级图形功能,包括点、线、圆、椭圆、圆弧、样条曲线,以及更加高级的逻辑画笔和逻辑画刷,必要时调用GAL接口完成加速功能;
尽管某些显示卡也提供有对上述高级绘图功能的硬件支持,但考虑到其他因素,这些硬件加速功能不由GAL接口提供;而统统通过软件实现。
这样,GAL主要实现的绘图功能限制在位块操作上,比如矩形填充和Blitting操作;而其他的高级图形功能,则全部由GDI函数实现。
3.2显示内存的有效利用
新的GAL接口能够有效利用显示卡上的显示内存,并充分利用硬件加速功能。我们知道,现在显示卡一般具有4M以上的显示内存,而一般的显示模式下,不会占用所有的显示内存。比如在显示模式为1204x768x32bpp时,一屏象素所占用的内存为3M,还有1M的内存可供应用程序使用。因此,新的GAL引擎能够管理这部分未被使用的显示内存,并分配给应用程序使用。这样,一方面可以节省系统内存的使用,另一方面,可以充分利用显示卡提供的加速功能,在显示内存的两个不同内存区域之间进行快速的位块操作,也就是常说的Blitting。
3.3Blitting 操作
在上层GDI接口在建立内存DC设备时,将首先在显示内存上分配内存,如果失败,才会考虑使用系统内存。这样,如果GAL引擎提供了硬件加速功能,两个不同DC设备之间的Blitting操作(即GDI函数BitBlt),将以最快的速度运行。更进一步,如果硬件支持透明或Alpha混和功能,则透明的或者Alpha混和的Blitting操作也将以最快的速度运行。新的GAL接口能够根据底层引擎的加速能力自动利用这些硬件加速功能。目前支持的硬件加速能力主要有:矩形填充,普通的Blitting操作,透明、Alpha混和的Blitting操作等。当然,如果硬件不支持这些加速功能,新的GAL接口也能够通过软件实现这些功能。目前通过GAL的FrameBuffer引擎提供上述硬件加速功能的显卡有:Matrox、3dfx等。
在通过软件实现透明或混和的DC间Blitting操作时,新的GAL接口利用了两种有效的优化措施:
在 i386平台上,充分利用嵌入式汇编代码进行优化处理;比如在处理32位色模式下的普通Blitting操作时,在利用普通的C库函数,即memcpy进行位块复制时,由于memcpy函数是以字节为单位进行复制的,从而无法利用32位CPU对32位字的处理能力,为此,可以使用嵌入式汇编,并以32位字为单位进行复制,这将大大提高Bliting操作的处理速度。
对源DC进行RLE(RunLength Encoding)编码,从而对象素的处理数量最小化。RLE可以看成是一种图象压缩算法,WindowsBMP 文件就利用了这种算法。RLE是按水平扫描线进行压缩编码处理的。在一条扫描线上,如果有大量相同的象素,则不会保存这些象素点,而是首先保存具有相同象素点的数目,然后保存这些象素点的值。这样,在进行透明或者混和的Blitting操作时,可以大大降低逐点运算带来的速度损失。但是,如果在最坏的情况下,比如所有水平扫描线上的象素点都具有和相邻点不同的象素值,则RLE编码反而会增加象素的存储空间(最坏的情况是原有空间的两倍),同时也会降低Blitting操作的速度。因此是否使用RLE编码,要根据情况而定。新的GDI接口在指定源DC的透明和Alpha通道值时,可以指定是否使用RLE编码。
3.4有效分辨率
新的GAL引擎可以设定一个不同于实际显示分辨率的有效分辨率。比如实际的显示分辨率是1024x768,则可以在/etc/MiniGUI.cfg文件中指定比实际分辨率低的有效分辨率。这种特性有利于在PC上调试需要运行在较小分辨率系统上的应用程序。比如:
[system]
gal_engine=native
ial_engine=native
[native]
defaultmode=320x240x16
其中在defaultmode当中指定了有效分辨率为320x240,16则表示颜色深度,即16位色,或者称为每象素的二进制位数。需要注意的是,对VESAFramBuffer 设备,必须指定和当前颜色深度一致的颜色深度值。对其他的FrameBuffer设备,如果能够支持多种显示模式,则会根据defaultmode指定的模式设置当前分辨率。
3.5新GAL的限制
需要注意的是,新的GAL结构只打算支持线性显示内存,并且只支持8位色以上的显示模式。如果要支持低于8位色的显示模式,则可以选择使用老的GAL和GDI接口。在配置MiniGUI的时候,你可以指定是否使用老的GAL和GDI接口。默认情况下的配置是使用新的GAL和GDI接口,需要使用老的GAL和GDI接口时,应进行如下的配置:
./configure--disable-newgal
另外,新的GAL接口支持Gamma校正和YUVOverlay,但目前尚未在GDI接口中体现这些功能。在新的版本中,会逐步添加相应的GDI接口。
4新的GDI接口
4.1新的区域算法
新的GDI采用了新的区域算法,即在XWindow 和其他GUI系统当中广泛使用的区域算法。这种区域称作"x-y-banned"区域,并且具有如下特点:
区域由互不相交的非空矩形组成;
区域又可以划分为若干互不相交的水平条带,每个水平条带中的矩形是等高,而且是上对齐的;或者说,这些矩形具有相同的高度,而且所有矩形的左上角y坐标相等。
区域中矩形的排列,首先是在x方向(在一个条带中)从左到右排列,然后按照y坐标从上到下排列。
在 GDI函数进行绘图输出时,可以利用x-y-banned区域的特殊性质进行绘图的优化。在将来版本中添加的绘图函数,将充分利用这一特性进行绘图输出上的优化。
新的 GDI增加了如下接口,可用于剪切区域的运算(include/gdi.h):
BOOLGUIAPI PtInRegion (PCLIPRGN region, int x, int y);
BOOL GUIAPIRectInRegion (PCLIPRGN region, const RECT* rect);
BOOLGUIAPI IntersectRegion (CLIPRGN *dst, const CLIPRGN *src1, constCLIPRGN *src2);
BOOL GUIAPI UnionRegion (PCLIPRGN dst, constCLIPRGN* src1, const CLIPRGN* src2);
BOOL GUIAPI SubtractRegion(CLIPRGN* rgnD, const CLIPRGN* rgnM, const CLIPRGN* rgnS);
BOOLGUIAPI XorRegion (CLIPRGN *dst, const CLIPRGN *src1, const CLIPRGN*src2);
PtInRegion函数可用来检查给定点是否位于给定的区域中。
RectInRegion函数可用来检查给定矩形是否和给定区域相交。
IntersectRegion函数对两个给定区域进行求交运算。
UnionRegion函数可合并两个不同的区域,合并后的区域仍然是x-y-banned的区域。
SubstractRegion函数从一个区域中减去另外一个区域。
XorRegion函数对两个区域进行异或运算,其结果相当于src1减src2的结果A与src2减src1的结果B之间的交。
在MiniGUI1.1.0 版本正式发布时,我们将添加从多边形、椭圆或圆弧等封闭曲线中生成剪切域的GDI函数。这样,就可以实现将GDI输出限制在特殊封闭曲线的效果。
4.2光栅操作
光栅操作是指在进行绘图输出时,如何将要输出的象素点和屏幕上已有的象素点进行运算。最典型的运算是下面要讲到的Alpha混和。这里的光栅操作特指二进制的位操作,包括与、或、异或和直接的设置(覆盖)等等。应用程序可以利用SetRasterOperation/GetRasterOperation函数设置或者获取当前的光栅操作。这两个函数的原型如下(include/gdi.h):
#defineROP_SET 0
#defineROP_AND 1
#defineROP_OR 2
#define ROP_XOR 3
intGUIAPI GetRasterOperation (HDC hdc);
int GUIAPI SetRasterOperation(HDC hdc, int rop);
在设置了新的光栅操作之后,其后的一般图形输出将受到设定的光栅操作的影响,这些图形输出包括:SetPixel、LineTo、Circle、Rectangle、FillRect和FillCircle等等。需要注意的是,新的GDI函数引入了一个新的矩形填充函数――FillRect。如上所述,FillRect函数是受当前光栅操作影响的,而原先的FillBox函数则不受当前的光栅操作影响。这是因为FillBox函数会利用硬件加速功能实现矩形填充,并且该函数的填充速度要比FillRect函数快。
4.3内存DC和BitBlt
新的GDI函数增强了内存DC操作函数。GDI函数在建立内存DC时,将调用GAL的相应接口。如前所述,GAL将尽量把内存DC建立在显示卡的显示内存当中。这样,可以充分利用显示卡的硬件加速功能,实现显示内存中两个不同区域之间位块的快速移动、复制等等,包括透明处理和Alpha混和。应用程序可以建立一个具有逐点Alpha特性的内存DC(每个点具有不同的Alpha值),也可以通过SetMemDCAlpha设置内存DC所有象素的Alpha值(或者称为"Alpha通道"),然后利用BitBlt和StretchBlt函数实现DC之间的位块传送。应用程序还可以通过SetMemDCColorKey函数设置源DC的透明色,从而在进行BitBlt时跳过这些透明色。
有关内存DC的GDI函数有(include/gdi.h):
#defineMEMDC_FLAG_NONE 0x00000000 /*None. */
#define MEMDC_FLAG_SWSURFACE 0x00000000 /*DC is in system memory */
#define MEMDC_FLAG_HWSURFACE 0x00000001 /*DC is in video memory */
#define MEMDC_FLAG_SRCCOLORKEY 0x00001000 /*Blit uses a source color key */
#define MEMDC_FLAG_SRCALPHA 0x00010000 /*Blit uses source alpha blending */
#define MEMDC_FLAG_RLEACCEL 0x00004000 /*Surface is RLE encoded */
HDCGUIAPI CreateCompatibleDC (HDC hdc);
HDC GUIAPI CreateMemDC (intwidth, int height, int depth, DWORD flags,
Uint32 Rmask, Uint32 Gmask, Uint32 Bmask, Uint32 Amask);
BOOLGUIAPI ConvertMemDC (HDC mem_dc, HDC ref_dc, DWORD flags);
BOOLGUIAPI SetMemDCAlpha (HDC mem_dc, DWORD flags, Uint8 alpha);
BOOLGUIAPI SetMemDCColorKey (HDC mem_dc, DWORD flags, Uint32color_key);
void GUIAPI DeleteMemDC (HDC mem_dc);
CreateCompatibleDC函数创建一个和给定DC兼容的内存DC。兼容的含义是指,新创建的内存DC的象素格式、宽度和高度与给定DC是相同的。利用这种方式建立的内存DC可以快速Blit到与之兼容的DC上。
这里需要对象素格式做进一步解释。象素格式包含了颜色深度(即每象素点的二进制位数)、调色板或者象素点中RGBA(红、绿、蓝、Alpha)四个分量的组成方式。其中的Alpha分量,可以理解为一个象素点的透明度,0表示完全透明,255表示完全不透明。在MiniGUI中,如果颜色深度低于8,则GAL会默认创建一个调色板,并且可以调用SetPalette函数修改调色板。如果颜色深度高于8,则通过四个变量分别指定象素点中RGBA分量所占的位。如果是建立兼容DC,则兼容内存DC和给定DC具有一样的颜色深度,同时具有一样的调色板或者一样的RGBA分量组成方式。
如果调用CreateMemDC函数,则可以指定新建内存DC的高度、宽度、颜色深度,以及必要的RGBA组成方式。在MiniGUI中,是通过各自在象素点中所占用的位掩码来表示RGBA四个分量的组成方式的。比如,如果要创建一个包含逐点Alpha信息的16位内存DC,则可以用每分量四个二进制位的方式分配16位的象素值,这样,RGBA四个分量的掩码分别为:0x0000F000,0x00000F00, 0x000000F0, 0x0000000F。
ConvertMemDC函数用来将一个任意的内存DC对象,根据给定的参考DC的象素格式进行转换,使得结果DC具有和参考DC一样的象素格式。这样,转换后的DC就能够快速Blit到与之兼容的DC上。
SetMemDCAlpha函数用来设定或者取消整个内存DC对象的Alpha通道值。我们还可以通过MEMDC_FLAG_RLEACCEL标志指定内存DC采用或者取消RLE编码方式。Alpha通道值将作用在DC的所有象素点上。
SetMemDCColorKey函数用来设定或者取消整个内存DC对象的ColorKey,即透明象素值。我们还可以通过MEMDC_FLAG_RLEACCEL标志指定内存DC采用或者取消RLE编码方式。
内存 DC和其他DC一样,也可以调用GDI的绘图函数向内存DC中进行任意的绘图输出,然后再BitBlt到其他DC中。下面的程序段演示了如何使用内存DC向窗口DC进行透明和Alpha混和的Blitting操作:
/*逐点Alpha操作*/
mem_dc = CreateMemDC (400, 100, 16, MEMDC_FLAG_HWSURFACE |MEMDC_FLAG_SRCALPHA,
0x0000F000, 0x00000F00, 0x000000F0, 0x0000000F);
/*设置一个不透明的刷子并填充矩形*/
SetBrushColor (mem_dc, RGBA2Pixel (mem_dc, 0xFF, 0xFF, 0x00,0xFF));
FillBox (mem_dc, 0, 0, 200, 50);
/*设置一个25% 透明的刷子并填充矩形*/
SetBrushColor (mem_dc, RGBA2Pixel (mem_dc, 0xFF, 0xFF, 0x00,0x40));
FillBox (mem_dc, 200, 0, 200, 50);
/*设置一个半透明的刷子并填充矩形*/
SetBrushColor (mem_dc, RGBA2Pixel (mem_dc, 0xFF, 0xFF, 0x00,0x80));
FillBox (mem_dc, 0, 50, 200, 50);
/*设置一个75%透明的刷子并填充矩形*/
SetBrushColor (mem_dc, RGBA2Pixel (mem_dc, 0xFF, 0xFF, 0x00,0xC0));
FillBox (mem_dc, 200, 50, 200, 50);
SetBkMode (mem_dc, BM_TRANSPARENT);
/*以半透明的象素点输出文字*/
SetTextColor (mem_dc, RGBA2Pixel (mem_dc, 0x00, 0x00, 0x00,0x80));
TabbedTextOut (mem_dc, 0, 0, "MemoryDC with alpha.\n"
"The source DC have alpha per-pixel.");
/*Blit 到窗口DC上*/
start_tick = GetTickCount ();
count = 100;
while (count--) {
BitBlt (mem_dc, 0, 0, 400, 100, hdc, rand () % 800, rand () %800);
}
end_tick =GetTickCount ();
TellSpeed (hwnd, start_tick,end_tick, "Alpha Blit", 100);
/*删除内存DC*/
DeleteMemDC (mem_dc);
/*具有Alpha通道的内存 DC:32位,RGB各占8位,无Alpha分量*/
mem_dc = CreateMemDC (400, 100, 32, MEMDC_FLAG_HWSURFACE |MEMDC_FLAG_SRCALPHA | MEMDC_FLAG_SRCCOLORKEY,
0x00FF0000, 0x0000FF00, 0x000000FF, 0x00000000);
/*输出填充矩形和文本到内存DC上*/
SetBrushColor (mem_dc, RGB2Pixel (mem_dc, 0xFF, 0xFF, 0x00));
FillBox (mem_dc, 0, 0, 400, 100);
SetBkMode(mem_dc, BM_TRANSPARENT);
SetTextColor (mem_dc,RGB2Pixel (mem_dc, 0x00, 0x00, 0xFF));
TabbedTextOut (mem_dc, 0, 0, "Memory DC withalpha.\n"
"The source DC have alpha per-surface.");
/*Blit 到窗口DC上*/
start_tick = GetTickCount ();
count = 100;
while (count--) {
/*设置内存DC的Alpha通道*/
SetMemDCAlpha (mem_dc, MEMDC_FLAG_SRCALPHA | MEMDC_FLAG_RLEACCEL,rand () % 256);
BitBlt(mem_dc, 0, 0, 400, 100, hdc, rand () % 800, rand () % 800);
}
end_tick = GetTickCount ();
TellSpeed (hwnd, start_tick, end_tick, "Alpha Blit", 100);
/*填充矩形区域,并输出文字 */
FillBox (mem_dc, 0, 0, 400, 100);
SetBrushColor(mem_dc, RGB2Pixel (mem_dc, 0xFF, 0x00, 0xFF));
TabbedTextOut (mem_dc, 0, 0, "Memory DC with alpha andcolorkey.\n"
"The source DC have alphaper-surface.\n"
"And the source DC have a colorkey,\n"
"and RLE accelerated.");
/*设置内存DC的透明象素值*/
SetMemDCColorKey (mem_dc, MEMDC_FLAG_SRCCOLORKEY |MEMDC_FLAG_RLEACCEL,
RGB2Pixel (mem_dc, 0xFF, 0xFF, 0x00));
/* Blit到窗口DC上*/
start_tick = GetTickCount ();
count = 100;
while (count--) {
BitBlt (mem_dc, 0, 0, 400, 100, hdc, rand () % 800, rand () %800);
CHECK_MSG;
}
end_tick = GetTickCount ();
TellSpeed (hwnd, start_tick, end_tick, "Alpha and colorkeyBlit", 100);
/*删除内存DC对象*/
DeleteMemDC (mem_dc);
4.4增强的BITMAP操作
新的GDI函数增强了BITMAP结构,添加了对透明和Alpha通道的支持。通过设置bmType、bmAlpha、bmColorkey等成员,就可以使得BITMAP对象具有某些属性。然后可以利用FillBoxWithBitmap/Part函数将BITMAP对象绘制到某个DC上。你可以将BITMAP对象看成是在系统内存中建立的内存DC对象,只是不能向这种内存DC对象进行绘图输出。下面的示例程序从图象文件中装载一个位图对象,然后设置透明和Alpha通道值,最后使用FillBoxWithBitmap函数输出到窗口DC上:
inttox = 800, toy = 800;
int count;
BITMAP bitmap;
unsigned int start_tick,end_tick;
if(LoadBitmap (hdc, &bitmap, "res/icon.bmp"))
return;
bitmap.bmType= BMP_TYPE_ALPHACHANNEL;
/*位图的Alpha混和*/
start_tick = GetTickCount ();
count = 1000;
while (count--) {
tox =rand() % 800;
toy =rand() % 800;
/*设置随机Alpha通道值*/
bitmap.bmAlpha = rand() % 256;
/* 显示到窗口DC上*/
FillBoxWithBitmap (hdc, tox, toy, 0, 0, &bitmap);
}
end_tick = GetTickCount ();
TellSpeed (hwnd, start_tick, end_tick, "Alpha Blended Bitmap",1000);
bitmap.bmType= BMP_TYPE_ALPHACHANNEL | BMP_TYPE_COLORKEY;
/* 取第一个象素点值,并设置为透明象素值*/
bitmap.bmColorKey = GetPixelInBitmap (&bitmap, 0, 0);
/*透明及Alpha混和*/
start_tick = GetTickCount ();
count = 1000;
while (count--) {
tox =rand() % 800;
toy =rand() % 800;
/* 设置一个随机Alpha通道值*/
bitmap.bmAlpha = rand() % 256;
/* 显示到窗口DC上*/
FillBoxWithBitmap (hdc, tox, toy, 0, 0, &bitmap);
}
end_tick = GetTickCount ();
TellSpeed (hwnd, start_tick, end_tick, "Alpha BlendedTransparent Bitmap", 1000);
UnloadBitmap(&bitmap);
你也可以通过CreateMemDCFromBitmap函数将某个BITMAP对象转换成内存DC对象。该函数的原型如下(src/gdi.h):
HDCGUIAPI CreateMemDCFromBitmap (HDC hdc, BITMAP* bmp);
需要注意的是,从BITMAP对象创建的内存DC直接使用BITMAP对象中的bmBits所指向的内存,该内存存在于系统内存,而不是显示内存中。
和 BITMAP相关的MYBITMAP结构,新的GDI也做了一些增强。MYBITMAP可以看成是设备无关的位图结构,你也可以利用CreateMemDCFromMyBitmap函数将一个MYBITMAP对象转换成内存DC。该函数的原型如下(src/gdi.h):
HDCGUIAPI CreateMemDCFromMyBitmap (HDC hdc, MYBITMAP* mybmp);
需要注意的是,许多GAL引擎不能对系统内存到显示内存的BitBlt操作提供硬件加速,所以,FillBoxWithBitmap函数,以及从BITMAP对象或者MYBITMAP对象创建的内存DC无法通过硬件加速功能快速BitBlt到其他DC上。如果希望达到这样的效果,可以通过预先创建的建立于显示内存中的DC进行快速的BitBlt运算。
4.5新的GDI绘图函数
除了光栅操作意外,还添加了一些有用的GDI绘图函数,包括FillRect、FillCircle等等,我们将在接下来的开发中,将继续添加诸如椭圆、圆弧、三次样条曲线、多边形填充等高级绘图函数。目前新增的GDI函数有:
voidGUIAPI FillRect (HDC hdc, int x, int y, int w, int h);
void GUIAPIFillCircle (HDC hdc, int sx, int sy, int r);
BOOLGUIAPI ScaleBitmap (BITMAP* dst, const BITMAP* src);
BOOLGUIAPI GetBitmapFromDC (HDC hdc, int x, int y, int w, int h, BITMAP*bmp);
gal_pixelGUIAPI GetPixelInBitmap (const BITMAP* bmp, int x, int y);
BOOLGUIAPI SetPixelInBitmap (const BITMAP* bmp, int x, int y, gal_pixelpixel);
FillRect函数填充指定矩形,受当前光栅操作影响。
FillCircle函数填充指定的圆,受当前光栅操作影响。
ScaleBitmap函数将源BITMAP对象进行伸缩处理。
GetBitmapFromDC函数将指定矩形范围内的象素复制到BITMAP对象中。
GetPixelInBitmap函数获得BITMAP对象中指定位置的象素值。
SetPixelInBitmap函数设置BITMAP对象中指定位置的象素值。
5其他
尽管在1.1.0Pre4以及其后版本对MiniGUI的GAL和GDI进行了大规模的改造,但在新版本中仍然可以利用老的GAL和GDI接口,从而提供对低端显示设备的支持。需要注意的是,虽然新GDIAPI 当中的许多结构和函数具有相同的名称,但某些函数已经被重新定义。所以,在编写应用程序的时候,要特别注意这一点。比如:新的mde演示程序当中,就利用了在<minigui/config.h>中定义的_USE_NEWGAL宏来判断是否使用新的GAL和GDI函数,如下所示:
#include<minigui/common.h>
#include <minigui/minigui.h>
#include<minigui/gdi.h>
......
#ifdef_USE_NEWGAL
SetRasterOperation (hdc,ROP_XOR);
FillRect (hdc, 0, 0, 200,200);
#else
/* Not implemented*/
#endif
......
6小结
本文重点介绍了在MiniGUI1.1.0 版本开发过程中新增的GAL、GDI功能和接口。新的GAL和GDI重点针对高端图形应用进行了优化和功能增强,其中包括透明处理、Alpha混和等高级特性,并且能够对硬件加速功能提供良好支持。本文分别就GAL和GDI的关系、GAL的功能特性、GDI的增强接口等方面较为全面地介绍了新的GAL和GDI接口。希望能够对程序开发有所帮助。
MiniGUI1.1.0引入的新GDI功能和函数(2)
1引言
我们在本系列主题五中曾经详细描述了在MiniGUI1.1.0 版本开发过程中添加的新GDI功能和函数。这些接口首次出现在版本1.1.0Pre4当中。目前MiniGUI1.1.0Pre7 版本已经发布,该版本中的新GDI接口趋于稳定,相对1.1.0Pre4版本而言,又新增了若干高级图形接口。这些接口涉及到直线和曲线生成器、复杂曲线的绘制、封闭曲线填充、复杂区域的创建、直接的显示缓冲区访问、YUV覆盖和Gamma校正等等。本文将就这些主题详细描述各个接口的用法。
2曲线和填充生成器
在一般的图形系统中,通常给用户提供若干用于进行直线或者复杂曲线,比如圆弧、椭圆和样条曲线的绘图函数。用户可以通过这些函数进行绘图,但不能利用这些系统中已有的曲线生成算法完成其他的工作。在MiniGUI新的GDI接口设计当中,我们采用了一种特殊的设计方法来实现曲线和封闭曲线的填充,这种方法非常灵活,而且给用户提供了直接使用系统内部算法的机会:
1)系统中定义了若干用来生成直线和曲线的函数,我们称之为"曲线生成器";
2)用户在调用生成器之前,需要定义一个回调函数,并将函数地址传递给曲线生成器,曲线生成器在生成了一个曲线上的点或者封闭曲线中的一条水平填充线时,将调用这个回调函数。
3)用户可以在回调函数当中完成针对新的点或者新的水平填充线的操作。对MiniGUI绘图函数来说,就是完成绘图工作。
4)因为回调函数在生成器的运行过程中不断调用,为了保持一致的上下文环境,系统允许用户在调用曲线生成器时传递一个表示上下文的指针,生成器将把该指针传递给回调函数。
下面将分小节讲述目前的MiniGUI版本所提供的曲线和填充生成器。
2.1直线剪切器和直线生成器
直线剪切器和生成器的原型如下:
/*Line clipper */
BOOL GUIAPI LineClipper (const RECT* cliprc, int*_x0, int *_y0, int *_x1, int *_y1);
/*Line generators */
typedef void (* CB_LINE) (void* context, intstepx, int stepy);
void GUIAPI LineGenerator (void* context, intx1, int y1, int x2, int y2, CB_LINE cb);
直线剪切器并不是生成器,它用于对给定的直线进行剪切操作。cliprc是给定的直线,而_x0、_y0、_x1和_y1传递要剪切的直线起始端点,并通过这些指针返回剪切之后的直线起始端点。MiniGUI内部使用了Cohen-Sutherland算法。
LineGenerator是采用Breshenham算法的生成器。该生成器从给定直线的起始端点开始,每生成一个点调用一次cb回调函数,并传递上下文context、以及新的点相对于上一个点的步进值或者差量。比如,传递stepx=1,stepy= 0 表示新的点比上一个点在X轴上前进一步,而在Y轴上保持不变。回调函数可以在步进值基础上实现某种程度上的优化。
2.2圆生成器
MiniGUI定义的圆生成器原型如下:
/*Circle generator */
typedef void (* CB_CIRCLE) (void* context, intx1, int x2, int y);
void GUIAPI CircleGenerator (void* context,int sx, int sy, int r, CB_CIRCLE cb);
首先要指定圆心坐标以及半径,并传递上下文信息以及回调函数,每生成一个点,生成器将调用一次cb回调函数,并传递三个值:x1、x2和y。这三个值实际表示了圆上的两个点:(x1,y) 和(x2,y)。因为圆的对称性,生成器只要计算圆上的四分之一圆弧点即可得出圆上所有的点。
2.3椭圆生成器
椭圆生成器和圆生成器类似,原型如下:
/*Ellipse generator */
typedef void (* CB_ELLIPSE) (void* context,int x1, int x2, int y);
void GUIAPI EllipseGenerator (void*context, int sx, int sy, int rx, int ry, CB_ELLIPSE cb);
首先要指定椭圆心坐标以及X轴和Y轴半径,并传递上下文信息以及回调函数,每生成一个点,生成器将调用一次cb回调函数,并传递三个值:x1、x2和y。这三个值实际表示了椭圆上的两个点:(x1,y) 和(x2,y)。因为椭圆的对称性,生成器只要计算椭圆上的二分之一圆弧点即可得出椭圆上所有的点。
2.4圆弧生成器
MiniGUI定义的圆弧生成器如下所示:
/*Arc generator */
typedef void (* CB_ARC) (void* context, int x,int y);
void GUIAPI ArcGenerator (void* context, int sx, int sy,int r, fixed ang1, fixed ang2, CB_ARC cb);
首先要指定圆弧的圆心、半径、起始弧度和终止弧度。需要注意的是,起始弧度和终止弧度是采用定点数表示的,而不是浮点数,并且是弧度而不是角度。然后传递cb回调函数。每生成一个圆弧上的点,该函数将调用回调函数,并传递新点的坐标值(x,y)。
有关定点数的信息,请参阅本系列"主题六:MiniGUI提供的非GUI/GDI接口"一文。
2.5垂直单调多边形生成器
通常而言,多边形有凸多边形和凹多边形之分。这里的垂直单调多边形,是为了优化多边形填充算法而针对计算机图形特点而提出的一种特殊多边形,这种多边形的定义如下:
垂直单调多边形是指,多边形的边和计算机屏幕上的所有水平扫描线,只能有一个或者两个交点,不会有更多交点。
图1给出了凸多边形、凹多边形和垂直单调多边形的几个示例。
需要注意的是,凸多边形一定是垂直单调多边形,但垂直单调多边形可以是凹多边形。显然,普通的多边形填充算法需要判断多边形边和每条屏幕扫描线之间的交点个数,而垂直单调多边形则可以免去这一判断,所以可以大大提高多边形填充的速度。
MiniGUI所定义的垂直单调多边形相关函数原型如下:
/*To determine whether the specified Polygon is Monotone VerticalPolygon */
BOOL GUIAPI PolygonIsMonotoneVertical (const POINT*pts, int vertices);
/*Monotone vertical polygon generator */
typedef void (* CB_POLYGON)(void* context, int x1, int x2, int y);
BOOL GUIAPIMonotoneVerticalPolygonGenerator (void* context, const POINT* pts,int vertices, CB_POLYGON cb);
PolygonIsMonotoneVertical用来判断给定的多边形是否是垂直单调多边形,而MonotoneVerticalPolygonGenerator函数是垂直多边形生成器。在MiniGUI当中,多边形是由组成多边形的顶点来表示的。pts表示顶点数组,而vertices表示顶点个数。生成器生成的实际是填充多边形的每一条水平线,端点为(x1,y) 和(x2,y)。
2.6一般矩形生成器
MiniGUI还提供了一般的矩形生成器,该生成器可以处理凸多边形,也可以处理凹多边形。原型如下:
/*General polygon generator */
typedef void (* CB_POLYGON) (void*context, int x1, int x2, int y);
BOOL GUIAPI PolygonGenerator(void* context, const POINT* pts, int vertices, CB_POLYGON cb);
和垂直单调多边形生成器一样,该函数生成的是填充多边形的每一条水平扫描线:x1是水平线的起始X坐标;x2是水平线的终止X坐标;y是水平线的Y坐标值。
2.7填注生成器
填注(floodfilling)生成器比较复杂。这个函数在MiniGUI内部用于FloodFill函数。我们知道,FloodFill函数从给定的起始位置开始,以给定的颜色向四面八方填充某个区域(像水一样蔓延,因此叫FloodFilling),一直到遇到与给定起始位置的象素值不同的点为止。因此,在这一过程中,我们需要两个回调函数,一个回调函数用来判断蔓延过程中遇到的点的象素值是否和起始点相同,另外一个回调函数用来生成填充该区域的水平扫描线。在进行绘图时,该函数比较的是象素值,但实际上,该函数也可以比较任何其他值,从而完成特有的蔓延动作。这就是将填注生成器单独出来的初衷。MiniGUI如下定义填注生成器:
/*General Flood Filling generator */
typedef BOOL (* CB_EQUAL_PIXEL)(void* context, int x, int y);
typedef void (* CB_FLOOD_FILL)(void* context, int x1, int x2, int y);
BOOL GUIAPIFloodFillGenerator (void* context, const RECT* src_rc, int x, inty,
CB_EQUAL_PIXEL cb_equal_pixel, CB_FLOOD_FILL cb_flood_fill);
cb_equal_pixel被调用,以便判断目标点的象素值是否和起始点一样,起始点的象素值可以通过context来传递。cb_flood_fill函数用来填充一条扫描线,传递的是水平扫描线的端点,即(x1,y) 和(x2,y)。
2.8曲线和填充生成器的用法
曲线和填充生成器的用法非常简单。为了对曲线和填充生成器有个更好的了解,我们首先看MiniGUI内部是如何使用曲线和填充生成器的。
下面的程序段来自MiniGUI的FloodFill函数(src/newgdi/flood.c):
staticvoid _flood_fill_draw_hline (void* context, int x1, int x2, inty)
{
PDC pdc = (PDC)context;
RECT rcOutput = {MIN (x1, x2), y, MAX (x1, x2) + 1, y + 1};
ENTER_DRAWING(pdc, rcOutput);
_dc_draw_hline_clip (context,x1, x2, y);
LEAVE_DRAWING (pdc, rcOutput);
}
staticBOOL equal_pixel (void* context, int x, int y)
{
gal_pixel pixel = _dc_get_pixel_cursor ((PDC)context, x, y);
return((PDC)context)->skip_pixel == pixel;
}
/*FloodFill
* Fills an enclosed area (starting at point x,y).
*/
BOOL GUIAPI FloodFill (HDC hdc, int x, int y)
{
PDC pdc;
BOOL ret = TRUE;
if(!(pdc = check_ecrgn (hdc)))
return TRUE;
/*hide cursor tempororily */
ShowCursor (FALSE);
coor_LP2SP(pdc, &x, &y);
pdc->cur_pixel= pdc->brushcolor;
pdc->cur_ban = NULL;
pdc->skip_pixel= _dc_get_pixel_cursor (pdc, x, y);
/*does the start point have a equal value? */
if(pdc->skip_pixel == pdc->brushcolor)
goto equal_pixel;
ret= FloodFillGenerator (pdc, &pdc->DevRC, x, y, equal_pixel,_flood_fill_draw_hline);
equal_pixel:
UNLOCK_GCRINFO (pdc);
/*Show cursor */
ShowCursor (TRUE);
returnret;
}
该函数在经过一些必要的初始化工作之后,调用FloodFillGenerator函数,并传递了上下文pdc(pdc是MiniGUI内部表示DC的数据结构)和两个回调函数地址:equal_pixel和_flood_fill_draw_hline函数。在这之前,该函数获得了起始点的象素值,并保存在了pdc->skip_pixel当中。equal_pixel函数获得给定点的象素值,然后返回与pdc->skip_pixel相比较之后的值;_flood_fill_draw_hline函数调用内部函数进行水平线的绘制。
读者可以看到,这种简单的生成器实现方式,能够大大降低代码复杂度,提高代码的重用能力。有兴趣的读者可以比较MiniGUI新老GDI接口的LineTo函数实现,相信能够得出一样的结论。
当然设计生成器的目的主要还是为方便用户使用。比如,你可以利用MiniGUI内部的曲线生成器完成自己的工作。下面的示例假定你使用圆生成器绘制一个线宽为4象素的圆:
staticvoid draw_circle_pixel (void* context, int x1, int x2, int y)
{
HDC hdc = (HDC) context;
/*以圆上的每个点为圆心,填充半径为2的圆。*/
FillCircle (hdc, x1, y, 2);
FillCircle (hdc,x2, y, 2);
}
voidDrawMyCircle (HDC hdc, int x, int y, int r, gal_pixel pixel)
{
gal_pixel old_brush;
old_bursh= SetBrushColor (hdc, pixle);
/*调用圆生成器*/
CircleGenerator ((void*)hdc, x, y, r, draw_circle_pixel);
/*恢复旧的画刷颜色*/
SetBrushColor (hdc, old_brush);
}
从上面的例子可以看出,曲线和填充生成器的用法极其简单,而且结构清晰明了。读者在自己的开发过程中,也可以学习这种方法。
3绘制复杂曲线
基于2中描述的曲线生成器,MiniGUI提供了如下基本的曲线绘制函数:
voidGUIAPI MoveTo (HDC hdc, int x, int y);
void GUIAPI LineTo (HDChdc, int x, int y);
void GUIAPI Rectangle (HDC hdc, int x0, inty0, int x1, int y1);
void GUIAPI PollyLineTo (HDC hdc, constPOINT* pts, int vertices);
void GUIAPI SplineTo (HDC hdc, constPOINT* pts);
void GUIAPI Circle (HDC hdc, int sx, int sy, intr);
void GUIAPI Ellipse (HDC hdc, int sx, int sy, int rx, intry);
void GUIAPI Arc (HDC hdc, int sx, int sy, int r, fixed ang1,fixed ang2);
MoveTo将当前画笔的起始点移动到给定点(x,y),以逻辑坐标指定。
LineTo从当前画笔点画直线到给定点(x,y),以逻辑坐标指定。
Rectangle函数画顶点为(x0,y0)和(x1,y0)的矩形。
PollyLineTo函数利用LineTo函数画折线。pts指定了折线的各个端点,vertices指定了折线端点个数。
SplineTo函数利用LineTo函数画三次样条曲线。需要注意的是,必须传递四个点才能惟一确定一条样条曲线,也就是说,pts是一个指向包含4个POINT结构数组的指针。
Circle函数绘制圆,圆心为(sx,sy),半径为r,以逻辑坐标指定。
Ellipse函数绘制椭圆,椭圆心为(sx,sy),X轴半径为rx,Y轴半径为ry。
Arc函数绘制圆弧,(sx,sy)指定了圆心,r指定半径,ang1和ang2指定圆弧的起始弧度和终止弧度。需要注意的是,ang1和ang2是以定点数形式指定的。
作为示例,我们看Circle和Ellipse函数的用法。假定给定了两个点,pts[0]和pts[1],其中pts[0]是圆心或者椭圆心,而pts[1]是圆或者椭圆外切矩形的一个顶点。下面的程序段绘制由这两个点给定的圆或者椭圆:
intrx = ABS (pts[1].x - pts[0].x);
int ry = ABS (pts[1].y - pts[0].y);
if(rx == ry)
Circle (hdc, pts[0].x, pts[0].y, rx);
else
Ellipse (hdc, pts[0].x, pts[0].y, rx, ry);
4封闭曲线填充
MiniGUI 目前提供了如下的封闭曲线填充函数:
voidGUIAPI FillBox (HDC hdc, int x, int y, int w, int h);
void GUIAPIFillCircle (HDC hdc, int sx, int sy, int r);
void GUIAPIFillEllipse (HDC hdc, int sx, int sy, int rx, int ry);
void GUIAPIFillSector (HDC hdc, int sx, int sy, int r, int ang1, int ang2);
BOOLGUIAPI FillPolygon (HDC hdc, const POINT* pts, int vertices);
BOOLGUIAPI FloodFill (HDC hdc, int x, int y);
FillBox函数填充指定的矩形。该矩形左上角顶点为(x,y),宽度为w,高度为h,以逻辑坐标指定。
FillCircle函数填充指定的圆。圆心为(sx,xy),半径为r,以逻辑坐标指定。
FillEllips函数填充指定的椭圆。椭圆心为(sx,sy),X轴半径为rx,Y轴半径为ry。
FillSector函数填充由圆弧和两条半径形成的扇形。圆心为(x,y),半径为r,起始弧度为ang1,终止弧度为ang2。
FillPolygon函数填充多边形。pts表示多边形各个顶点,vertices表示多边形顶点个数。
FloodFill从指定点(x,y)开始填注。
需要注意的是,所有填充函数使用当前画刷属性(颜色),并且受当前光栅操作的影响。
下面的例子说明了如何使用FillCircle和FillEllipse函数填充圆或者椭圆。假定给定了两个点,pts[0]和pts[1],其中pts[0]是圆心或者椭圆心,而pts[1]是圆或者椭圆外切矩形的一个顶点。
intrx = ABS (pts[1].x - pts[0].x);
int ry = ABS (pts[1].y - pts[0].y);
if(rx == ry)
FillCircle (hdc, pts[0].x, pts[0].y, rx);
else
FillEllipse (hdc, pts[0].x, pts[0].y, rx, ry);
5建立复杂区域
除了利用填充生成器进行填充绘制以外,我们还可以使用填充生成器建立由封闭曲线包围的复杂区域。我们知道,MiniGUI当中的区域是由互不相交的矩形组成的,并且满足x-y-banned的分布规则。利用上述的多边形或者封闭曲线生成器,可以将每条扫描线看成是组成区域的高度为1的一个矩形,这样,我们可以利用这些生成器建立复杂区域。MiniGUI利用现有的封闭曲线生成器,实现了如下的复杂区域生成函数:
BOOLGUIAPI InitCircleRegion (PCLIPRGN dst, int x, int y, int r);
BOOLGUIAPI InitEllipseRegion (PCLIPRGN dst, int x, int y, int rx, intry);
BOOL GUIAPI InitPolygonRegion (PCLIPRGN dst, const POINT*pts, int vertices);
BOOL GUIAPI InitSectorRegion (PCLIPRGN dst,const POINT* pts, int vertices);
利用这些函数,我们可以将某个区域分别初始化为圆、椭圆、多边形和扇形区域。然后,可以利用这些区域进行点击测试(PtInRegion和RectInRegion),或者选择到DC当中作为剪切域,从而获得特殊显示效果。
6直接访问显示缓冲区
在新的 GDI接口中,我们添加了用来直接访问显示缓冲区的函数,原型如下:
Uint8*GUIAPI LockDC (HDC hdc, const RECT* rw_rc, int* width, int* height,int* pitch);
void GUIAPI UnlockDC (HDC hdc);
LockDC函数锁定给定HDC的指定矩形区域(由矩形rw_rc指定,设备坐标),然后返回缓冲区头指针。当width、height、pitch三个指针不为空时,该函数将返回锁定之后的矩形有效宽度、有效高度和每扫描线所占的字节数。
UnlockDC 函数解开已锁定的HDC。
锁定一个HDC意味着MiniGUI进入以互斥方式访问显示缓冲区的状态。如果被锁定的HDC是一个屏幕DC(即非内存DC),则该函数将在必要时隐藏鼠标光标,并锁定HDC对应的全局剪切域。在锁定一个HDC之后,程序可通过该函数返回的指针对锁定区域进行访问。需要注意的是,不能长时间锁定一个HDC,也不应该在锁定一个HDC时进行其他额外的系统调用。
假定以锁定矩形左上角为原点建立坐标系,X轴水平向右,Y轴垂直向下,则可以通过如下的公式计算该坐标系中(x,y)点对应的缓冲区地址(假定该函数返回的指针值为frame_buffer):
Uint8*pixel_add = frame_buffer + y * (*pitch) + x * GetGDCapability (hdc,GDCAP_BPP);
根据该HDC的颜色深度,就可以对该象素进行读写操作。作为示例,下面的程序段随机填充锁定区域:
inti, width, height, pitch;
RECT rc = {0, 0, 200,200};
int bpp = GetGDCapability (hdc,GDCAP_BPP);
Uint8* frame_buffer = LockDC (hdc,&rc, &width, &height, &pitch);
Uint8* row = frame_buffer;
for(i = 0; i < *height; i++) {
memset (row, rand ()%0x100, *width * bpp);
row += *pitch;
}
UnlockDC(hdc);
7YUV 覆盖和Gamma校正
为了增强MiniGUI对多媒体的支持,我们增加了对YUV覆盖(Overlay)和Gamma校正的支持。
7.1YUV 覆盖(Overlay)
多媒体领域中,尤其在涉及到MPEG播放时,通常使用YUV颜色空间来表示颜色,如果要在屏幕上显示一副MPEG解压之后的图片,则需要进行YUV颜色空间到RGB颜色空间的转换。YUV覆盖最初来自一些显示芯片的加速功能。这种显示芯片能够在硬件基础上完成YUV到RGB的转换,免去软件转换带来的性能损失。在这种显示芯片上建立了YUV覆盖之后,可以直接将YUV信息写入缓冲区,硬件能够自动完成YUV到RGB的转换,从而在RGB显示器上显示出来。在不支持YUV覆盖的显示芯片上,MiniGUI也能够通过软件实现YUV覆盖,这时,需要调用DisplayYUVOverlay函数将YUV信息转换并缩放显示在建立YUV覆盖的DC设备上。
MiniGUI提供的YUV覆盖操作函数原型如下:
/*****************************YUV overlay support ***************************/
/*最常见的视频覆盖格式.
*/
#defineGAL_YV12_OVERLAY 0x32315659 /* Planar mode: Y+ V + U (3 planes) */
#define GAL_IYUV_OVERLAY 0x56555949 /* Planar mode: Y + U + V (3planes) */
#define GAL_YUY2_OVERLAY 0x32595559 /* Packed mode: Y0+U0+Y1+V0 (1 plane) */
#define GAL_UYVY_OVERLAY 0x59565955 /* Packed mode: U0+Y0+V0+Y1 (1 plane)*/
#define GAL_YVYU_OVERLAY 0x55595659 /*Packed mode: Y0+V0+Y1+U0 (1 plane) */
/*该函数创建一个视频输出覆盖
*/
GAL_Overlay*GUIAPI CreateYUVOverlay (int width, int height,
Uint32 format, HDC hdc);
/*锁定覆盖进行直接的缓冲区读写,结束后解锁*/
intGAL_LockYUVOverlay (GAL_Overlay *overlay);
voidGAL_UnlockYUVOverlay (GAL_Overlay *overlay);
#defineLockYUVOverlay GAL_LockYUVOverlay
#define UnlockYUVOverlayGAL_UnlockYUVOverlay
/*释放视频覆盖*/
voidGAL_FreeYUVOverlay (GAL_Overlay *overlay);
#define FreeYUVOverlayGAL_FreeYUVOverlay
/*将视频覆盖传送到指定DC设备上。该函数能够进行2维缩放
*/
voidGUIAPI DisplayYUVOverlay (GAL_Overlay* overlay, const RECT* dstrect);
有关视频格式的信息,可参见:
http://www.webartz.com/fourcc/indexyuv.htm
有关颜色空间的相互关系的息,可参见:
http://www.neuro.sfc.keio.ac.jp/~aly/polygon/info/color-space-faq.html
7.2Gamma 校正
Gamma校正通过为RGB颜色空间的每个颜色通道设置Gamma因子,来动态调整RGB显示器上的实际RGB效果。需要注意的是,Gamma校正需要显示芯片的硬件支持。
应用程序可以通过SetGamma函数设置RGB三个颜色通道的Gamma校正值。该函数原型如下:
intGAL_SetGamma (float red, float green, float blue);
#defineSetGamma GAL_SetGamma
线性 Gamma校正值的范围在0.1到10.0之间。如果硬件不支持Gamma校正,该函数将返回-1。
应用程序也可以通过SetGammaRamp函数设置RGB三个颜色通道的非线性Gamma校正值。该函数原型如下:
intGAL_SetGammaRamp (Uint16 *red, Uint16 *green, Uint16 *blue);
#defineSetGammaRamp GAL_SetGammaRamp
intGAL_GetGammaRamp (Uint16 *red, Uint16 *green, Uint16 *blue);
#defineGetGammaRamp GAL_GetGammaRamp
函数SetGammaRamp实际设置的是每个颜色通道的Gamma转换表,每个表由256个值组成,表示设置值和实际值之间的对应关系。当设置屏幕上某个象素的RGB分别为R、G、B时,实际在显示器上获得的象素RGB值分别为:red[R]、green[G]、blue[B]。如果硬件不支持Gamma校正,该函数将返回-1。
函数GetGammaRamp获得当前的Gamma转换表。
Gamma校正的最初目的,是为了能够在显示器上精确还原一副图片。Gamma值在某种程度上表示的是某个颜色通道的对比度变化。但Gamma在多媒体和游戏程序中有一些特殊用途――通过Gamma校正,可以方便地获得对比度渐进效果。
8小结
本文描述了自MiniGUI1.1.0Pre4 版本发布以来新增的GDI接口。这些接口涉及到曲线和填充生成器、复杂曲线的绘制、封闭曲线填充、复杂区域的创建、直接访问FrameBuffer、YUV覆盖和Gamma校正等等。通过本文的介绍,相信读者能够对MiniGUI的新GDI接口有一个更加全面的认识。
MiniGUI提供的非GUI/GDI接口
1引言
一般而言,GUI系统的应用程序编程接口主要集中于窗口、消息队列、图形设备等相关方面。但因为GUI系统在处理系统事件时通常会提供自己的机制,而这些机制往往会和操作系统本身提供的机制不相兼容。比如,MiniGUI提供了消息循环机制,而应用程序的结构一般是消息驱动的;也就是说,应用程序通过被动接收消息来工作。但很多情况下,应用程序需要主动监视某个系统事件,比如在UNIX操作系统中,可以通过select系统调用监听某个文件描述符上是否有可读数据。这样,如何将MiniGUI的消息队列机制和现有操作系统的其他机制融合在一起,就成了一个较为困难的问题。本文将讲述几种解决这一问题的方法。
我们知道,MiniGUI-Lite采用UNIXDomain Socket实现客户程序和服务器程序之间的交互。应用程序也可以利用这一机制,完成自己的通讯任务――客户向服务器提交请求,而服务器完成对客户的请求处理并应答。一方面,在MiniGUI-Lite的服务器程序中,你可以扩展这一机制,注册自己的请求处理函数,完成定制的请求/响应通讯任务。另一方面,MiniGUI-Lite当中也提供了若干用来创建和操作UNIXDomain Socket 的函数,任何MiniGUI-Lite的应用程序都可以建立UNIXDomain Socket,并完成和其他MiniGUI-Lite应用程序之间的数据交换。本文将举例讲述如何利用MiniGUI-Lite提供的函数完成此类通讯任务。
嵌入式Linux系统现在能够在许多不同架构的硬件平台上运行,MiniGUI也能够在这些硬件平台上运行。但由于许多硬件平台具有和其他硬件平台不同的特性,比如说,常见的CPU是LittleEndian 的,而某些CPU则是BigEndian 的。这要求我们在编写代码,尤其是文件I/O相关代码时,必须编写可移植代码,以便适合具有不同架构的平台。本文将描述MiniGUI为应用程序提供的可移植性函数及其用法。
除了与上述内容相关的函数之外,MiniGUI还提供了其他一些函数,本文最后部分将描述这些函数的用途和用法,包括配置文件读写以及定点数运算。
2MiniGUI-Lite和select系统调用
我们知道,在MiniGUI-Lite之上运行的应用程序只有一个消息队列。应用程序在初始化之后,会建立一个消息循环,然后不停地从这个消息队列当中获得消息并处理,直到接收到MSG_QUIT消息为止。应用程序的窗口过程在处理消息时,要在处理完消息之后立即返回,以便有机会获得其他的消息并处理。现在,如果应用程序在处理某个消息时监听某个文件描述符而调用select系统调用,就有可能会出现问题――因为select系统调用可能会长时间阻塞,而由MiniGUI-Lite服务器发送给客户的事件得不到及时处理。这样,消息驱动的方式和select系统调用就难于很好地融合。在MiniGUI-Threads中,因为每个线程都有自己相应的消息队列,而系统消息队列是由单独运行的desktop线程管理的,所以任何一个应用程序建立的线程都可以长时间阻塞,从而可以调用类似select的系统调用。但在MiniGUI-Lite当中,如果要监听某个应用程序自己的文件描述符事件,必须进行恰当的处理,以避免长时间阻塞。
在MiniGUI-Lite当中,有几种解决这一问题的办法:
在调用select系统调用时,传递超时值,保证select系统调用不会长时间阻塞。
设置定时器,定时器到期时,利用select系统调用查看被监听的文件描述符。如果没有相应的事件发生,则立即返回,否则进行读写操作。
利用MiniGUI-Lite提供的RegisterListenFD函数在系统中注册监听文件描述符,并在被监听的文件描述符上发生指定的事件时,向某个窗口发送MSG_FDEVENT消息。
由于前两种解决方法比较简单,这里我们重点讲述的第三种解决办法。MiniGUI-Lite为应用程序提供了如下两个函数及一个宏:
#defineMAX_NR_LISTEN_FD 5
/*Return TRUE if all OK, and FALSE on error. */
BOOL GUIAPIRegisterListenFD (int fd, int type, HWND hwnd, void* context);
/*Return TRUE if all OK, and FALSE on error. */
BOOL GUIAPIUnregisterListenFD (int fd);
MAX_NR_LISTEN_FD宏定义了系统能够监听的最多文件描述符数,默认定义为5。
RegisterListenFD函数在系统当中注册一个需要监听的文件描述符,并指定监听的事件类型(type参数,可取POLLIN、POLLOUT或者POLLERR),接收MSG_FDEVENT消息的窗口句柄以及一个上下文信息。
UnregisterListenFD函数注销一个被注册的监听文件描述符。
在应用程序使用RegisterListenFD函数注册了被监听的文件描述符之后,当指定的事件发生在该文件描述符上时,系统会将MSG_FDEVENT消息发送到指定的窗口,应用程序可在窗口过程中接收该消息并处理。MiniGUI中的libvcongui就利用了上述函数监听来自主控伪终端上的可读事件,如下面的程序段所示(vcongui/vcongui.c):
...
/*注册主控伪终端伪监听文件描述符*/
RegisterListenFD (pConInfo->masterPty, POLLIN, hMainWnd, 0);
/*进入消息循环*/
while (!pConInfo->terminate && GetMessage (&Msg,hMainWnd)) {
DispatchMessage (&Msg);
}
/* 注销监听文件描述符*/
UnregisterListenFD (pConInfo->masterPty);
...
/*虚拟控制台的窗口过程*/
staticint VCOnGUIMainWinProc (HWND hWnd, int message, WPARAM wParam, LPARAMlParam)
{
PCONINFO pConInfo;
pConInfo= (PCONINFO)GetWindowAdditionalData (hWnd);
switch (message) {
...
/*接收到MSG_FDEVENT消息,则处理主控伪终端上的输入数据*/
case MSG_FDEVENT:
ReadMasterPty (pConInfo);
break;
...
}
/*调用默认窗口过程*/
if (pConInfo->DefWinProc)
return (*pConInfo->DefWinProc)(hWnd, message, wParam, lParam);
else
returnDefaultMainWinProc (hWnd, message, wParam, lParam);
}
在 3.2节当中,我们还可以看到RegisterListenFD函数的使用。显然,通过这种简单的注册监听文件描述符的接口,MiniGUI-Lite程序能够方便地利用底层的消息机制完成对异步事件的处理。
3MiniGUI-Lite 与进程间通讯
3.1简单请求/应答处理
我们知道,MiniGUI-Lite利用了UNIXDomain Socket 实现服务器和客户程序之间的通讯。为了实现客户和服务器之间的简单方便的通讯,MiniGUI-Lite中定义了一种简单的请求/响应结构。客户程序通过指定的结构将请求发送到服务器,服务器处理请求并应答。在客户端,一个请求定义如下(include/gdi.h):
typedefstruct tagREQUEST {
int id;
const void* data;
size_t len_data;
}REQUEST;
typedef REQUEST* PREQUEST;
其中,id是用来标识请求类型的整型数,data是发送给该请求的关联数据,len_data则是数据的长度。客户在初始化REQUEST结构之后,就可以调用cli_request向服务器发送请求,并等待服务器的应答。该函数的原型如下。
/*send a request to server and wait reply */
int cli_request(PREQUEST request, void* result, int len_rslt);
服务器程序(即mginit)会在自己的消息循环当中获得来自客户的请求,并进行处理,最终会将处理结果发送给客户。
在上述这种简单的客户/服务器通讯中,客户和服务器必须就每个请求类型达成一致,也就是说,客户和服务器必须了解每种类型请求的数据含义并进行恰当的处理。
MiniGUI-Lite利用上述这种简单的通讯方法,实现了若干系统级的通讯任务:
鼠标光标的管理。鼠标光标是一个全局资源,当客户需要创建或者销毁鼠标光标,改变鼠标光标的形状、位置,显示或者隐藏鼠标时,就发送请求到服务器,服务器程序完成相应任务并将结果发送给客户。
层及活动客户管理。当客户查询层的信息,新建层,加入某个已有层,或者设置层中的活动客户时,通过该接口发送请求到服务器。
其他一些系统级的任务。比如在新的GDI接口中,服务器程序统一管理显示卡中可能用来建立内存DC的显示内存,当客户要申请建立在显示内存中的内存DC时,就会发送请求到服务器。
为了让应用程序也能够通过这种简单的方式实现客户和服务器之间的通讯,服务器程序可以注册一些定制的请求处理函数,然后客户就可以向服务器发送这些请求。为此,MiniGUI-Lite提供了如下接口:
#defineMAX_SYS_REQID 0x0010
#define MAX_REQID 0x0018
/*
*Register user defined request handlers for server
* Note that userdefined request id should larger than MAX_SYS_REQID
*/
typedefint (* REQ_HANDLER) (int cli, int clifd, void* buff, size_tlen);
BOOL GUIAPI RegisterRequestHandler (int req_id, REQ_HANDLERyour_handler);
REQ_HANDLER GUIAPI GetRequestHandler (int req_id);
服务器可以通过调用RegisterRequestHandler函数注册一些请求处理函数。注意请求处理函数的原型由REQ_HANDLER定义。还要注意系统定义了MAX_SYS_REQID和MAX_REQID这两个宏。MAX_REQID是能够注册的最大请求ID号,而MAX_SYS_REQID是系统内部使用的最大的请求ID号,也就是说,通过RegisterRequestHandler注册的请求ID号,必须大于MAX_SYS_REQID而小于或等于MAX_REQID。
作为示例,我们假设服务器替客户计算两个整数的和。客户发送两个整数给服务器,而服务器将两个整数的和发送给客户。下面的程序段在服务器程序中运行,在系统中注册了一个请求处理函数:
typedefstruct TEST_REQ
{
int a, b;
} TEST_REQ;
staticint send_reply (int clifd, void* reply, int len)
{
MSG reply_msg = {HWND_INVALID, 0};
/*发送一个空消息接口给客户,以便说明这是一个请求的应答*/
if (sock_write (clifd, &reply_msg, sizeof (MSG)) < 0)
return SOCKERR_IO;
/*将结果发送给客户*/
if (sock_write (clifd, reply, len) < 0)
return SOCKERR_IO;
returnSOCKERR_OK;
}
staticint test_request (int cli, int clifd, void* buff, size_t len)
{
int ret_value = 0;
TEST_REQ* test_req =(TEST_REQ*)buff;
ret_value= test_req.a + test_req.b;
returnsend_reply (clifd, &ret_value, sizeof (int));
}
...
RegisterRequestHandler (MAX_SYS_REQID + 1, test_request);
...
而客户程序可以通过如下的程序段向客户发送一个请求获得两个整数的和:
REQUEST req;
TEST_REQtest_req = {5, 10};
intret_value;
req.id= MAX_SYS_REQID + 1;
req.data = &rest_req;
req.len_data = sizeof (TEST_REQ);
cli_request(&req, &ret_value, sizeof (int));
printf ("the returned value: %d\n", ret_value); /* ret_value 的值应该是15*/
读者已经看到,通过这种简单的请求/应答技术,MiniGUI-Lite客户程序和服务器程序之间可以建立一种非常方便的进程间通讯机制。但这种技术也有一些缺点,比如受到MAX_REQID大小的影响,通讯机制并不是非常灵活,而且请求只能发送给MiniGUI-Lite的服务器程序(即mginit)处理等等。
3.2复杂的UNIXDomain Socket 封装
为了解决上述简单请求/应答机制的不足,MiniGUI-Lite也提供了经过封装的UNIXDomain Socket 处理函数。这些函数的接口原型如下(include/minigui.h):
/*Used by server to create a listen socket.
* Name is the name oflisten socket.
* Please located the socket in /var/tmp directory.*/
/*Returns fd if all OK, -1 on error. */
int serv_listen (const char*name);
/*Wait for a client connection to arrive, and accept it.
* We alsoobtain the client's pid and user ID from the pathname
* that itmust bind before calling us. */
/*returns new fd if all OK, < 0 on error */
int serv_accept (intlistenfd, pid_t *pidptr, uid_t *uidptr);
/*Used by clients to connect to a server.
* Name is the name of thelisten socket.
* The created socket will located at the directory/var/tmp,
* and with name of '/var/tmp/xxxxx-c', where 'xxxxx' isthe pid of client.
* and 'c' is a character to distinguishdiferent projects.
* MiniGUI use 'a' as the project character.
*/
/*Returns fd if all OK, -1 on error. */
int cli_conn (const char*name, char project);
#defineSOCKERR_IO -1
#define SOCKERR_CLOSED -2
#defineSOCKERR_INVARG -3
#defineSOCKERR_OK 0
/*UNIX domain socket I/O functions. */
/*Returns SOCKERR_OK if all OK, < 0 on error.*/
int sock_write_t(int fd, const void* buff, int count, unsigned int timeout);
intsock_read_t (int fd, void* buff, int count, unsigned int timeout);
#definesock_write(fd, buff, count) sock_write_t(fd, buff, count, 0)
#definesock_read(fd, buff, count) sock_read_t(fd, buff, count, 0)
上述函数是MiniGUI-Lite用来建立系统内部使用的UNIXDomain Socket 并进行数据传递的函数,是对基本套接字系统调用的封装。这些函数的功能描述如下:
serv_listen:服务器调用该函数建立一个监听套接字,并返回套接字文件描述符。建议将服务器监听套接字建立在/var/tmp/目录下。
serv_accept:服务器调用该函数接受来自客户的连接请求。
cli_conn:客户调用该函数连接到服务器,其中name是客户的监听套接字。该函数为客户建立的套接字将保存在/var/tmp/目录中,并且以-c的方式命名,其中c是用来区别不同套接字通讯用途的字母,由project参数指定。MiniGUI-Lite内部使用了'a',所以由应用程序建立的套接字,应该使用除'a'之外的字母。
sock_write_t:在建立并连接之后,客户和服务器之间就可以使用sock_write_t函数和sock_read_t函数进行数据交换。sock_write_t的参数和系统调用write类似,但可以传递进入一个超时参数,注意该参数以10ms为单位,为零时超时设置失效,且超时设置只在mginit程序中有效。
sock_read_t:sock_read_t的参数和系统调用read类似,但可以传递进入一个超时参数,注意该参数以10ms为单位,为零时超时设置失效,且超时设置只在mginit程序中有效。
下面的代码演示了作为服务器的程序如何利用上述函数建立一个监听套接字:
#defineLISTEN_SOCKET "/var/tmp/mysocket"
staticint listen_fd;
BOOLlisten_socket (HWND hwnd)
{
if ((listen_fd =serv_listen (LISTEN_SOCKET)) < 0)
return FALSE;
return RegisterListenFD (fd,POLL_IN, hwnd, NULL);
}
当服务器接收到来自客户的连接请求是,服务器的hwnd窗口将接收到MSG_FDEVENT消息,这时,服务器可接受该连接请求:
intMyWndProc (HWND hwnd, int message, WPARAM wParam, LPARAMlParam)
{
switch (message) {
...
caseMSG_FDEVENT:
if (LOWORD (wParam) == listen_fd) { /* 来自监听套接字*/
pid_t pid;
uid_t uid;
int conn_fd;
conn_fd = serv_accept (listen_fd, &pid, &uid);
if (conn_fd >= 0) {
RegisterListenFD (conn_fd, POLL_IN, hwnd, NULL);
}
}
else { /* 来自已连接套接字*/
int fd = LOWORD(wParam);
/* 处理来自客户的数据*/
sock_read_t (fd, ...);
sock_write_t (fd, ....);
}
break;
...
}
}
上面的代码中,服务器将连接得到的新文件描述符也注册为监听描述符,因此,在MSG_FDEVENT消息的处理中,应该判断导致MSG_FDEVENT消息的文件描述符类型,并做适当的处理。
在客户端,当需要连接到服务器时,可通过如下代码:
int conn_fd;
if((conn_fd = cli_conn (LISTEN_SOCKET, 'b')) >= 0) {
/* 向服务器发送请求*/
sock_write_t (fd, ....);
/* 获取来自服务器的处理结果*/
sock_read_t (fd, ....);
}
4编写可移植代码
我们知道,许多嵌入式系统所使用的CPU具有和普通台式机CPU完全不同的构造和特点。但有了操作系统和高级语言,可以最大程度上将这些不同隐藏起来。只要利用高级语言编程,编译器和操作系统能够帮助程序员解决许多和CPU构造及特点相关的问题,从而节省程序开发时间,并提高程序开发效率。然而某些CPU特点却是应用程序开发人员所必须面对的,这其中就有如下几个需要特别注意的方面:
字节顺序。一般情况下,我们接触到的CPU在存放多字节的整数数据时,将低位字节存放在低地址单元中,比如常见的Intelx86 系列CPU。而某些CPU采用相反的字节顺序。比如在嵌入式系统中使用较为广泛的PowerPC就将低位字节存放在高地址单元中。前者叫LittleEndian 系统;而后者叫BigEndian 系统。
在某些平台上的Linux内核,可能缺少某些高级系统调用,最常见的就是与虚拟内存机制相关的系统调用。在某些CPU上运行的Linux操作系统,因为CPU能力的限制,无法提供虚拟内存机制,基于虚拟内存实现的某些IPC机制就无法正常工作。比如在某些缺少MMU单元的CPU上,就无法提供SystemV IPC 机制中的共享内存。
为了编写具有最广泛适应性的可移植代码,应用程序开发人员必须注意到这些不同,并且根据情况编写可移植代码。这里,我们将描述如何在MiniGUI应用程序中编写可移植代码。
4.1理解并使用MiniGUI的Endian读写函数
为了解决上述的第一个问题,MiniGUI提供了若干Endian相关的读写函数。这些函数可以划分为如下两类:
用来交换字节序的函数。包括ArchSwapLE16、ArchSwapBE16等。
用来读写标准I/O流的函数。包括MGUI_ReadLE16、MGUI_ReadBE16等。
前一类用来将某个16位、32位或者64位整数从某个特定的字节序转换为系统私有(native)字节序。举例如下:
int fd, len_header;
...
if(read (fd, &len_header, sizeof (int)) == -1)
goto error;
#if MGUI_BYTEORDER == MGUI_BIG_ENDIAN
len_header = ArchSwap32 (len_header); // 如果是BigEndian 系统,则转换字节序
#endif
...
在上面的程序段中,首先通过read系统调用从指定的文件描述符中读取一个整数值到len_header变量中。该文件中保存的整数值是LittleEndian 的,因此如果在BigEndian 系统上使用这个整数值,就必须进行字节顺序交换。这里可以使用ArchSwapLE32,将LittleEndian 的32位整数值转换为系统私有的字节序。也可以如上述程序段那样,只对BigEndian 系统进行字节序转换,这时,只要利用ArchSwap32函数即可。
MiniGUI提供的用来转换字节序的函数(或者宏)如下:
ArchSwapLE16(X)将指定的以LittleEndian 字节序存放的16位整数值转换为系统私有整数值。如果系统本身是LittleEndian 系统,则该函数不作任何工作,直接返回X;如果系统本身是BigEndian 系统,则调用ArchSwap16函数交换字节序。
ArchSwapLE32(X)将指定的以LittleEndian 字节序存放的32位整数值转换为系统私有整数值。如果系统本身是LittleEndian 系统,则该函数不作任何工作,直接返回X;如果系统本身是BigEndian 系统,则调用ArchSwap32函数交换字节序。
ArchSwapBE16(X)将指定的以BigEndian 字节序存放的16位整数值转换为系统私有整数值。如果系统本身是BigEndian 系统,则该函数不作任何工作,直接返回X;如果系统本身是LittleEndian 系统,则调用ArchSwap16函数交换字节序。
ArchSwapBE32(X)将指定的以BigEndian 字节序存放的32位整数值转换为系统私有整数值。如果系统本身是BigEndian 系统,则该函数不作任何工作,直接返回X;如果系统本身是LittleEndian 系统,则调用ArchSwap32函数交换字节序。
MiniGUI提供的第二类函数用来从标准I/O的文件对象中读写Endian整数值。如果要读取的文件是以LittleEndian 字节序存放的,则可以使用MGUI_ReadLE16和MGUI_ReadLE32等函数读取整数值,这些函数将把读入的整数值转换为系统私有字节序,反之使用MGUI_ReadBE16和MGUI_ReadBE32函数。如果要写入的文件是以LittleEndian 字节序存放的,则可以使用MGUI_WriteLE16和MGUI_WriteLE32等函数读取整数值,这些函数将把要写入的整数值从系统私有字节序转换为LittleEndian 字节序,然后写入文件,反之使用MGUI_WriteBE16和MGUI_WriteBE32函数。下面的代码段说明了上述函数的用法:
FILE* out;
int ount;
...
MGUI_WriteLE32 (out, count); // 以LittleEndian 字节序保存count到文件中。
...
4.2利用条件编译编写可移植代码
在涉及到可移植性问题的时候,有时我们能够方便地通过4.1中描述的方法进行函数封装,从而提供具有良好移植性的代码,但有时我们无法通过函数封装的方法提供可移植性代码。这时,恐怕只能使用条件编译了。下面的代码说明了如何使用条件编译的方法确保程序正常工作(该代码来自MiniGUIsrc/kernel/sharedres.c):
/*如果系统不支持共享内存,则定义_USE_MMAP
#undef _USE_MMAP
/* #define _USE_MMAP 1 */
void*LoadSharedResource (void)
{
#ifndef _USE_MMAP
key_t shm_key;
void *memptr;
int shmid;
#endif
/*装载共享资源*/
...
#ifndef_USE_MMAP /* 获取共享内存对象*/
if ((shm_key = get_shm_key ()) == -1) {
goto error;
}
shmid =shmget (shm_key, mgSizeRes, SHM_PARAM | IPC_CREAT | IPC_EXCL);
if (shmid == -1) {
gotoerror;
}
//Attach to the share memory.
memptr = shmat(shmid, 0, 0);
if (memptr == (char*)-1)
goto error;
else {
memcpy (memptr, mgSharedRes, mgSizeRes);
free (mgSharedRes);
}
if(shmctl (shmid, IPC_RMID, NULL) < 0)
goto error;
#endif
/*打开文件*/
if ((lockfd = open (LOCKFILE, O_WRONLY | O_CREAT | O_TRUNC, 0644)) ==-1)
goto error;
#ifdef_USE_MMAP
/* 如果使用mmap,就将共享资源写入文件*/
if (write (lockfd, mgSharedRes, mgSizeRes) < mgSizeRes)
goto error;
else
{
free(mgSharedRes);
mgSharedRes = mmap( 0, mgSizeRes, PROT_READ|PROT_WRITE, MAP_SHARED,lockfd, 0);
}
#else
/*否则将共享内存对象ID写入文件*/
if (write (lockfd, &shmid, sizeof (shmid)) < sizeof(shmid))
gotoerror;
#endif
close(lockfd);
#ifndef_USE_MMAP
mgSharedRes = memptr;
SHAREDRES_SHMID = shmid;
#endif
SHAREDRES_SEMID = semid;
returnmgSharedRes;
error:
perror ("LoadSharedResource");
returnNULL;
}
上述程序段是MiniGUI-Lite服务器程序用来装载共享资源的。如果系统支持共享内存,则初始化共享内存对象,并将装载的共享资源关联到共享内存对象,然后将共享内存对象ID写入文件;如果系统不支持共享内存,则将初始化后的共享资源全部写入文件。在客户端,如果支持共享内存,则可以从文件中获得共享内存对象ID,并直接关联到共享内存;如果不支持共享内存,则可以使用mmap系统调用,将文件映射到进程的地址空间。客户端的代码段如下:
void*AttachSharedResource (void)
{
#ifndef _USE_MMAP
int shmid;
#endif
int lockfd;
void* memptr;
if((lockfd = open (LOCKFILE, O_RDONLY)) == -1)
goto error;
#ifdef_USE_MMAP
/* 使用mmap将共享资源映射到进程地址空间*/
mgSizeRes = lseek (lockfd, 0, SEEK_END );
memptr = mmap( 0, mgSizeRes, PROT_READ, MAP_SHARED, lockfd,0);
#else
/* 否则获取共享内存对象ID,并关联该共享内存*/
if (read (lockfd, &shmid, sizeof (shmid)) < sizeof(shmid))
gotoerror;
close (lockfd);
memptr= shmat (shmid, 0, SHM_RDONLY);
#endif
if(memptr == (char*)-1)
goto error;
return memptr;
error:
perror ("AttachSharedResource");
return NULL;
}
5其他
5.1读写配置文件
MiniGUI的配置文件,即/etc/MiniGUI.cfg文件的格式,采用了类似WindowsINI文件的格式。这种文件格式非常简单,如下所示:
[section-name1]
key-name1=key-value1
key-name2=key-value2
[section-name2]
key-name3=key-value3
key-name4=key-value4
这种配置文件中的参数以section分组,然后用key=value的形式指定参数及其值。应用程序也可以利用这种配置文件格式保存一些配置信息,为此,MiniGUI提供了如下三个函数(include/minigui.h):
intGUIAPI GetValueFromEtcFile (const char* pEtcFile, const char*pSection,const char* pKey, char* pValue, int iLen);
int GUIAPIGetIntValueFromEtcFile (const char* pEtcFile, const char*pSection,const char* pKey, int* value);
int GUIAPISetValueToEtcFile (const char* pEtcFile, const char* pSection, constchar* pKey, char* pValue);
这三个函数的用途如下:
·GetValueFromEtcFile:从指定的配置文件当中获取指定的键值,键值以字符串形式返回。
·GetIntValueFromEtcFile:从指定的配置文件当中获取指定的整数型键值。该函数将获得的字符串转换为整数值返回(采用strtol函数转换)。
·SetValueToEtcFile:该函数将给定的键值保存到指定的配置文件当中,如果配置文件不存在,则将新建配置文件。如果给定的键已存在,则将覆盖旧值。
假定某个配置文件记录了一些应用程序信息,并具有如下格式:
[mginit]
nr=8
autostart=0
[app0]
path=../tools/
name=vcongui
layer=
tip=Virtual&console&on&MiniGUI
icon=res/konsole.gif
[app1]
path=../bomb/
name=bomb
layer=
tip=Game&of&Minesweaper
icon=res/kmines.gif
[app2]
path=../controlpanel/
name=controlpanel
layer=
tip=Control&Panel
icon=res/kcmx.gif
其中的[mginit]段记录了应用程序个数(nr键),以及自动启动的应用程序索引(autostart键)。而[appX]段记录了每个应用程序的信息,包括该应用程序的路径、名称、图标等等。下面的代码演示了如何使用MiniGU的配置文件函数获取这些信息(该代码段来自mde演示包中的mginit程序):
#defineAPP_INFO_FILE "mginit.rc"
staticBOOL get_app_info (void)
{
int i;
APPITEM* item;
/*获取应用程序个数信息*/
if (GetIntValueFromEtcFile (APP_INFO_FILE, "mginit", "nr",&app_info.nr_apps) != ETC_OK)
return FALSE;
if(app_info.nr_apps <= 0)
return FALSE;
/*获取自动启动的应用程序索引*/
GetIntValueFromEtcFile (APP_INFO_FILE, "mginit","autostart", &app_info.autostart);
if(app_info.autostart >= app_info.nr_apps || app_info.autostart <0)
app_info.autostart =0;
/*分配应用程序信息结构*/
if ((app_info.app_items = (APPITEM*)calloc (app_info.nr_apps, sizeof(APPITEM))) == NULL) {
return FALSE;
}
/*获取每个应用程序的路径、名称、图标等信息*/
item = app_info.app_items;
for (i = 0; i <app_info.nr_apps; i++, item++) {
char section [10];
sprintf(section, "app%d", i);
if (GetValueFromEtcFile (APP_INFO_FILE, section, "path",item->path, PATH_MAX) != ETC_OK)
goto error;
if(GetValueFromEtcFile (APP_INFO_FILE, section, "name",item->name, NAME_MAX) != ETC_OK)
goto error;
if(GetValueFromEtcFile (APP_INFO_FILE, section, "layer",item->layer, LEN_LAYER_NAME) != ETC_OK)
goto error;
if(GetValueFromEtcFile (APP_INFO_FILE, section, "tip",item->tip, TIP_MAX) != ETC_OK)
goto error;
strsubchr(item->tip, '&', ' ');
if(GetValueFromEtcFile (APP_INFO_FILE, section, "icon",item->bmp_path, PATH_MAX + NAME_MAX) != ETC_OK)
goto error;
if(LoadBitmap (HDC_SCREEN, &item->bmp, item->bmp_path) !=ERR_BMP_OK)
goto error;
item->cdpath= TRUE;
}
return TRUE;
error:
free_app_info ();
return FALSE;
}
5.2定点数运算
通常在进行数学运算时,我们采用浮点数表示实数,并利用头文件中所声明的函数进行浮点数运算。我们知道,浮点数运算是一种非常耗时的运算过程。为了减少因为浮点数运算而带来的额外CPU指令,在一些三维图形库当中,通常会采用定点数来表示实数,并利用定点数进行运算,这样,将大大提高三维图形的运算速度。MiniGUI也提供了一些定点数运算函数,分为如下几类:
整数、浮点数和定点数之间的转换。利用itofix和fixtoi函数可实现整数和定点数之间的相互转换;利用ftofix和fixtof函数可实现浮点数和定点数之间的转换。
定点数加、减、乘、除等基本运算。利用fadd、fsub、fmul、fdiv、fsqrt等函数可实现定点数加、减、乘、除以及平方根运算。
定点数的三角运算。利用fcos、fsin、ftan、facos、fasin等函数可求给定定点数的余弦、正弦、正切、反余弦、反正弦值。
矩阵、向量等运算。矩阵、向量相关运算在三维图形中非常重要,限于篇幅,本文不会详细讲述这些运算,读者可参阅MiniGUI的include/fixedmath.h头文件。
下面的代码段演示了定点数的用法,该程序段根据给定的三个点(pts[0]、pts[1]、pts[2])画一个弧线,其中pts[0]作为圆心,pts[1]是圆弧的起点,而pts[2]是圆弧终点和圆心连线上的一个点:
voiddraw_arc (HDC hdc, POINT* pts)
{
int sx = pts [0].x, sy = pts [0].y;
int dx = pts [1].x - sx, dy = pts [1].y - sy;
int r = sqrt (dx * dx * 1.0 + dy * dy * 1.0);
double cos_d = dx * 1.0 / r;
fixed cos_f = ftofix (cos_d);
fixed ang1 = facos (cos_f);
int r2;
fixed ang2;
if(dy > 0) {
ang1 = fsub (0, ang1);
}
dx= pts [2].x - sx;
dy = pts [2].y - sy;
r2 = sqrt (dx * dx * 1.0 + dy * dy * 1.0);
cos_d = dx * 1.0 / r2;
cos_f = ftofix (cos_d);
ang2 = facos (cos_f);
if (dy > 0) {
ang2 = fsub (0, ang2);
}
Arc(hdc, sx, sy, r, ang1, ang2);
}
上述程序的计算非常简单,步骤如下(该程序段来自mde演示程序包中的painter/painter.c程序):
根据 pts[0]和pts[1]计算圆弧的半径,然后计算圆弧的起始偏角,即ang1,使用了ftofix函数和facos函数。
计算pts[2]点和圆心连线的夹角,即ang2,使用了ftofix和facos函数。
调用Arc函数绘制圆弧。
6小结
本文讲述了MiniGUI为应用程序提供的一些非GUI/GDI的接口。这些接口中,某些是为了解决和操作系统的交互而设计的,以便MiniGUI应用程序能够更好地与操作系统提供的机制融合在一起;而某些提供了对UNIXDomain Socket良好封装的接口,可帮助应用程序方便进行进程间通讯或者扩展其功能;其他接口则专注于嵌入式系统的特殊性,为应用程序提供了可移植的文件I/O封装代码。在这些接口的帮助下,嵌入式系统开发人员可以编写功能强大而灵活的应用程序。
MiniGUI和其他嵌入式Linux上的图形及图形用户界面系统
1Linux 图形领域的基础设施
本小节首先向读者描述Linux图形领域中常见的基础设施。之所以称为基础设施,是因为这些系统(或者函数库),一般作为其他高级图形或者图形应用程序的基本函数库。这些系统(或者函数库)包括:XWindow、SVGALib、FrameBuffer等等。
1.1X Window
提起Linux上的图形,许多人首先想到的是XWindow。这一系统是目前类UNIX系统中处于控制地位的桌面图形系统。无疑,XWindow 作为一个图形环境是成功的,它上面运行着包括CAD建模工具和办公套件在内的大量应用程序。但必须看到的是,由于XWindow 在体系接口上的原因,限制了其对游戏、多媒体的支持能力。用户在XWindow 上运行VCD播放器,或者运行一些大型的三维游戏时,经常会发现同样的硬件配置,却不能获得和Windows操作系统一样的图形效果――即使使用了加速的XServer,其效果也不能令人满意。另外,大型的应用程序(比如Mozilla浏览器)在XWindow 上运行时的响应能力,也相当不能令人满意。当然,这里有Linux内核在进程调度上的问题,也有XWindow 的原因。
XWindow 为了满足对游戏、多媒体等应用对图形加速能力的要求,提供了DGA(直接图形访问)扩展,通过该扩展,应用程序可以在全屏模式下直接访问显示卡的帧缓冲区,并能够提供对某些加速功能的支持。
Tiny-X是XServer在嵌入式系统的小巧实现,它由Xfree86Core Team 的KeithPackard开发。它的目标是运行于小内存系统环境。典型的运行于X86CPU 上的Tiny-XServer 尺寸接近(小于)1MB。
1.2SVGALib
SVGALib 是Linux系统中最早出现的非X图形支持库。这个库从最初对标准VGA兼容芯片的支持开始,一直发展到对老式SVGA芯片的支持以及对现今流行的高级视频芯片的支持。它为用户提供了在控制台上进行图形编程的接口,使用户可以在PC兼容系统上方便地获得图形支持。但该系统有如下不足:
接口杂乱。SVGALib从最初的vgalib发展而来,保留了老系统的许多接口,而这些接口却不能良好地迎合新显示芯片的图形能力。
未能较好地隐藏硬件细节。许多操作,不能自动使用显示芯片的加速能力支持。
可移植性差。SVGALib目前只能运行在x86平台上,对其他平台的支持能力较差(Alpha平台除外)。
发展缓慢,有被其他图形库取代的可能。SVGALib作为一个老的图形支持库,目前的应用范围越来越小,尤其在Linux内核增加了FrameBuffer驱动支持之后,有逐渐被其他图形库替代的迹象。
对应用的支持能力较差。SVAGLib作为一个图形库,对高级图形功能的支持,比如直线和曲线等等,却不能令人满意。尽管SVGALib有许多缺点,但SVGALib经常被其他图形库用来初始化特定芯片的显示模式,并获得映射到进程地址空间的线性显示内存首地址(即帧缓冲区),而其他的接口却很少用到。另外,SVGALib中所包含的诸如键盘、鼠标和游戏杆的接口,也很少被其他应用程序所使用。
因此,SVGALib的使用越来越少,笔者也不建议用户使用这个图形库。当然,如果用户的显示卡只支持标准VGA模式,则SVGALib还是比较好的选择。
1.3FrameBuffer
FrameBuffer 是出现在2.2.xx内核当中的一种驱动程序接口。这种接口将显示设备抽象为帧缓冲区。用户可以将它看成是显示内存的一个映像,将其映射到进程地址空间之后,就可以直接进行读写操作,而写操作可以立即反应在屏幕上。该驱动程序的设备文件一般是/dev/fb0、/dev/fb1等等。比如,假设现在的显示模式是1024x768-8位色,则可以通过如下的命令清空屏幕:
$dd if=/dev/zero of=/dev/fb0 bs=1024 count=768
在应用程序中,一般通过将FrameBuffer设备映射到进程地址空间的方式使用,比如下面的程序就打开/dev/fb0设备,并通过mmap系统调用进行地址映射,随后用memset将屏幕清空(这里假设显示模式是1024x768-8位色模式,线性内存模式):
intfb;
unsigned char* fb_mem;
fb= open ("/dev/fb0", O_RDWR);
fb_mem = mmap (NULL,1024*768, PROT_READ|PROT_WRITE,MAP_SHARED,fb,0);
memset(fb_mem, 0, 1024*768);
FrameBuffer 设备还提供了若干ioctl命令,通过这些命令,可以获得显示设备的一些固定信息(比如显示内存大小)、与显示模式相关的可变信息(比如分辨率、象素结构、每扫描线的字节宽度),以及伪彩色模式下的调色板信息等等。
通过FrameBuffer设备,还可以获得当前内核所支持的加速显示卡的类型(通过固定信息得到),这种类型通常是和特定显示芯片相关的。比如目前最新的内核(2.4.9)中,就包含有对S3、Matrox、nVidia、3Dfx等等流行显示芯片的加速支持。在获得了加速芯片类型之后,应用程序就可以将PCI设备的内存I/O(memio)映射到进程的地址空间。这些memio一般是用来控制显示卡的寄存器,通过对这些寄存器的操作,应用程序就可以控制特定显卡的加速功能。
PCI设备可以将自己的控制寄存器映射到物理内存空间,而后,对这些控制寄存器的访问,给变成了对物理内存的访问。因此,这些寄存器又被称为"memio"。一旦被映射到物理内存,Linux的普通进程就可以通过mmap将这些内存I/O映射到进程地址空间,这样就可以直接访问这些寄存器了。
当然,因为不同的显示芯片具有不同的加速能力,对memio的使用和定义也各自不同,这时,就需要针对加速芯片的不同类型来编写实现不同的加速功能。比如大多数芯片都提供了对矩形填充的硬件加速支持,但不同的芯片实现方式不同,这时,就需要针对不同的芯片类型编写不同的用来完成填充矩形的函数。
说到这里,读者可能已经意识到FrameBuffer只是一个提供显示内存和显示芯片寄存器从物理内存映射到进程地址空间中的设备。所以,对于应用程序而言,如果希望在FrameBuffer之上进行图形编程,还需要完成其他许多工作。举个例子来讲,FrameBuffer就像一张画布,使用什么样子的画笔,如何画画,还需要你自己动手完成。
1.4LibGGI
LibGGI试图建立一个一般性的图形接口,而这个抽象接口连同相关的输入(鼠标、键盘、游戏杆等)抽象接口一起,可以方便地运行在XWindow、SVGALib、FrameBuffer等等之上。建立在LibGGI之上的应用程序,不经重新编译,就可以在上述这些底层图形接口上运行。但不知何故,LibGGI的发展几乎停滞。
2Linux 图形领域的高级函数库
2.1Xlib 及其他相关函数库
在XWindow 系统中进行图形编程时,可以选择直接使用Xlib。Xlib实际是对底层X协议的封装,可通过该函数库进行一般的图形输出。如果你的XServer 支持DGA,则可以通过DGA扩展直接访问显示设备,从而获得加速支持。对一般用户而言,由于Xlib的接口太原始而且复杂,因此一般的图形程序选择其他高级一些的图形库作为基础。比如,GTK、QT等等。这两个函数同时还是一些高级的图形用户界面支持函数库。由于种种原因,GTK、QT等函数库存在有庞大、占用系统资源多的问题,不太适合在嵌入式系统中使用。这时,你可以选择使用FLTK,这是一个轻量级的图形函数库,但它的主要功能集中在用户界面上,提供了较为丰富的控件集。
2.2SDL
SDL(SimpleDirectMediaLayer)是一个跨平台的多媒体游戏支持库。其中包含了对图形、声音、游戏杆、线程等等的支持,目前可以运行在许多平台上,其中包括XWindow、XWindow with DGA、LinuxFrameBuffer 控制台、LinuxSVGALib,以及WindowsDirectX、BeOS等等。
因为 SDL专门为游戏和多媒体应用而设计开发,所以它对图形的支持非常优秀,尤其是高级图形能力,比如Alpha混和、透明处理、YUV覆盖、Gamma校正等等。而且在SDL环境中能够非常方便地加载支持OpenGL的Mesa库,从而提供对二维和三维图形的支持。
可以说,SDL是编写跨平台游戏和多媒体应用的最佳平台,也的确得到了广泛应用。相关信息,可参阅http://www.libsdl.org。
2.3Allegro
Allegro 是一个专门为x86平台设计的游戏图形库。最初的Allegro运行在DOS环境下,而目前可运行在LinuxFrameBuffe 控制台、LinuxSVGALib、XWindow 等系统上。Allegro提供了一些丰富的图形功能,包括矩形填充和样条曲线生成等等,而且具有较好的三维图形显示能力。由于Allegro的许多关键代码是采用汇编编写的,所以该函数库具有运行速度快、资源占用少的特点。然而,Allegro也存在如下缺点:
·对线程的支持较差。Allegro的许多函数是非线程安全的,不能同时在两个以上的线程中使用。
·硬件加速能力的支持不足,在设计上没有为硬件加速提供接口。
有关Allegro的进一步信息,可参阅http://www.allegro.cc/。
2.4Mesa3D
Mesa3D 是一个兼容OpenGL规范的开放源码函数库,是目前Linux上提供专业三维图形支持的惟一选择。Mesa3D同时也是一个跨平台的函数库,能够运行在XWindow、XWindow with DGA、BeOS、LinuxSVGALib 等平台上。
有关 Mesa3D的进一步信息,可参阅http://www.mesa3d.org/。
2.5DirectFB
DirectFB 是专注于LinuxFrameBuffer 加速的一个图形库,并试图建立一个兼容GTK的嵌入式GUI系统。它以可装载函数库的形势提供对加速FrameBuffer驱动程序的支持。目前,该函数库正在开发之中(最新版本0.9.97),详情可见http://www.directfb.org/。
3面向嵌入式Linux系统的图形用户界面
3.1MicoroWindows/NanoX
MicroWindows(http://microwindows.censoft.com)是一个开放源码的项目,目前由美国CenturySoftware 公司主持开发。该项目的开发一度非常活跃,国内也有人参与了其中的开发,并编写了GB2312等字符集的支持。但在Qt/Embedded发布以来,该项目变得不太活跃,并长时间停留在0.89Pre7版本。可以说,以开放源码形势发展的MicroWindows项目,基本停滞。
MicroWindows是一个基于典型客户/服务器体系结构的GUI系统,基本分为三层。最底层是面向图形输出和键盘、鼠标或触摸屏的驱动程序;中间层提供底层硬件的抽象接口,并进行窗口管理;最高层分别提供兼容于XWindow 和WindowsCE(Win32子集)的API。
该项目的主要特色在于提供了类似X的客户/服务器体系结构,并提供了相对完善的图形功能,包括一些高级的功能,比如Alpha混合,三维支持,TrueType字体支持等。但需要注意的是,MicroWindows的图形引擎存在许多问题,可以归纳如下:
无任何硬件加速能力。
图形引擎中存在许多低效算法,同时未经任何优化。比如在直线或者圆弧绘图函数中,存在低效的逐点判断剪切的问题。
代码质量较差。由于该项目缺少一个强有力的核心代码维护人员,因此代码质量参差不齐,影响整体系统稳定性。这也是MicroWindows长时间停留在0.89Pre7版本上的原因。
MicroWindows采用MPL条款发布(该条款基本类似LGPL条款)。
3.2OpenGUI
OpenGUI(http://www.tutok.sk/fastgl/)在Linux系统上存在已经很长时间了。最初的名字叫FastGL,只支持256色的线性显存模式,但目前也支持其他显示模式,并且支持多种操作系统平台,比如MS-DOS、QNX和Linux等等,不过目前只支持x86硬件平台。OpenGUI也分为三层。最低层是由汇编编写的快速图形引擎;中间层提供了图形绘制API,包括线条、矩形、圆弧等,并且兼容于Borland的BGIAPI。第三层用C++编写,提供了完整的GUI对象集。
OpenGUI采用LGPL条款发布。OpenGUI比较适合于基于x86平台的实时系统,可移植性稍差,目前的发展也基本停滞。
3.3Qt/Embedded
Qt/Embedded是著名的Qt库开发商TrollTech(http://www.trolltech.com/)发布的面向嵌入式系统的Qt版本。因为Qt是KDE等项目使用的GUI支持库,所以有许多基于Qt的XWindow 程序可以非常方便地移植到Qt/Embedded版本上。因此,自从Qt/Embedded以GPL条款形势发布以来,就有大量的嵌入式Linux开发商转到了Qt/Embedded系统上。比如韩国的Mizi公司,台湾省的某些嵌入式Linux应用开发商等等。
不过,在笔者看来,Qt/Embedded还有一些问题值得开发者注意:
目前,该系统采用两种条款发布,其中包括GPL条款。对函数库使用GPL条款,意味着其上的应用需要遵循GPL条款。当然了,如果要开发商业程序,TrollTech也允许你采用另外一个授权条款,这时,就必须向TrollTech交纳授权费用了。
Qt/Embedded 是一个C++函数库,尽管Qt/Embedded声称可以裁剪到最少630K,但这时的Qt/Embedded库已经基本上失去了使用价值。低的程序效率、大的资源消耗也对运行Qt/Embedded的硬件提出了更高的要求。
Qt/Embedded库目前主要针对手持式信息终端,因为对硬件加速支持的匮乏,很难应用到对图形速度、功能和效率要求较高的嵌入式系统当中,比如机顶盒、游戏终端等等。
Qt/Embedded 提供的控件集风格沿用了PC风格,并不太适合许多手持设备的操作要求。
Qt/Embedded 的结构过于复杂,很难进行底层的扩充、定制和移植,尤其是那个用来实现signal/slot机制的著名的moc文件。
因为上述这些原因,目前所见到的Qt/Embedded的运行环境,几乎是清一色基于StrongARM的iPAQ。
3.4MiniGUI
MiniGUI(http://www.minigui.org)是由笔者主持,并由许多自由软件开发人员支持的一个自由软件项目(遵循LGPL条款发布),其目标是为基于Linux的实时嵌入式系统提供一个轻量级的图形用户界面支持系统。该项目自1998年底开始到现在,已历经3年多的开发过程。到目前为止,已经非常成熟和稳定。目前,我们已经正式发布了稳定版本1.0.9,并且开始了新版本系列的开发,即MiniGUIVersion 1.1.x,该系列的正式版也即将发布。
在 MiniGUI几年的发展过程中,有许多值得一提的技术创新点,正是由于这些技术上的创新,才使得MiniGUI更加适合实时嵌入式系统;而且MiniGUI的灵活性非常好,可以应用在包括手持设备、机顶盒、游戏终端等等在内的各种高端或者低端的嵌入式系统当中。这些技术创新包括:
图形抽象层。图形抽象层对顶层API基本没有影响,但大大方便了MiniGUI应用程序的移植、调试等工作。目前包含三个图形引擎,SVGALib、LibGGI以及直接基于LinuxFrameBuffer 的NativeEngine,利用LibGGI时,可在XWindow 上运行MiniGUI应用程序,并可非常方便地进行调试。与图形抽象层相关的还有输入事件的抽象层。MiniGUI现在已经被证明能够在基于ARM、MIPS、StrongARM以及PowerPC等的嵌入式系统上流畅运行。
多字体和多字符集支持。这部分通过设备上下文(DC)的逻辑字体(LOGFONT)实现,不管是字体类型还是字符集,都可以非常方便地进行扩充。应用程序在启动时,可切换系统字符集,比如GB、BIG5、EUCKR、UJIS。利用DrawText等函数时,可通过指定字体而获得其他字符集支持。对于一个窗口来说,同时显示不同语种的文字是可能的。MiniGUI的这种字符集支持不同于传统通过UNICODE实现的多字符集支持,这种实现更加适合于嵌入式系统。
两个不同架构的版本。最初的MiniGUI运行在PThread库之上,这个版本适合于功能单一的嵌入式系统,但存在系统健壮性不够的缺点。在0.9.98版本中,我们引入了MiniGUI-Lite版本,这个版本在提高系统健壮性的同时,通过一系列创新途径,避免了传统C/S结构的弱点,为功能复杂的嵌入式系统提供了一个高效、稳定的GUI系统。
在 MiniGUI1.1.0 版本的开发中,我们参照SDL和Allegro的图形部分,重新设计了图形抽象层,并增强了图形功能,同时增强了MiniGUI-Lite版本的某些特性。这些特性包括:
MiniGUI-Lite支持层的概念。同一层可容纳多个能够同时显示的客户程序,并平铺在屏幕上显示。
新的GAL能够支持硬件加速能力,并能够充分使用显示内存;新GAL之上的新GDI接口得到进一步增强。新的GDI接口可以支持Alpha混和、透明位块传输、光栅操作、YUV覆盖、Gamma校正,以及高级图形功能(椭圆、多边形、样条曲线)等等。
MiniGUI新版本在图形方面的增强和提高,将大大扩展它的应用领域,希望能够对嵌入式Linux上的多媒体应用、游戏开发提供支持。
纵观嵌入式Linux系统上的各种图形系统方案,我们发现,许多图形系统(如Qt/Embedded和MicoroWindows),只注重手持设备上的需求,却不太注重其他应用领域的需求,而其他许多需要图形支持的嵌入式Linux系统却需要许多独特的、高级的图形功能,而不仅仅是图形用户界面。为此,在接下来的开发中,我们还将在如下领域继续开发MiniGUI:
·提供运行在MiniGUI上的JAVA虚拟机AWT组件的实现。
·提供MiniGUI上的OpenGL实现。
·提供类QT控件集的C++封装。
·提供窗口/控件风格主题支持。
·在MiniGUI-Lite当中增加对矢量字体的支持。
4小结
综上所述,笔者认为在嵌入式Linux图形领域,还有许多有待开发人员仔细研究和解决的问题。MiniGUI的新的发展,也正源于对这些需求的认识之上。我们也衷心希望能够有更多的自由软件开发人员加盟MiniGUI的开发,一同开发新的嵌入式Linux的图形系统。
Lite的新改进
MiniGUI从0.98开始推出Lite版本。Lite版本是MiniGUI迈向嵌入式应用重要的一步。在Lite版本中,我们使用了自己设计的引擎,抛弃了pthread库,从而使得MiniGUI能够轻装上阵,更稳定,更高效率,也更符合嵌入式系统应用。本文介绍了MiniGUILite版本的基于UnixIPC实现的多进程机制。并详细介绍了一些实现细节。
1引言:为什么要开发Lite版本
现在,大多数UNIX系统采用X窗口系统作为图形用户界面,MSWindows 则采用Microsoft公司自己设计的GUI系统。这两种GUI系统也代表着目前通用GUI系统的两种实现。比如,著名的自由软件MicroWindows就同时实现了类似于MSWindows的MicroWindowsAPI 和类似于XWindow的NanoXAPI。
MiniGUI原来就采用了类似于MSWindows的体系结构,并且建立了基于线程的消息传递和窗口管理机制。然而,它是基于POSIX线程的,这种实现提供最大程度上的数据共享,但也同时造成MiniGUI体系结构上的脆弱。如果某个线程因为非法的数据访问而终止运行,则整个系统都将受到影响。
另一种方法是采用UNIX进程间通信机制建立窗口系统,即类似XWindow 的客户/服务器体系。但是这种体系结构也有它的先天不足,主要是通常的IPC机制无法提供高效的数据复制,大量的CPU资源用于各进程之间复制数据。在PDA等设备中,这种CPU资源的浪费将最终导致系统性能的降低以及设备耗电量的增加。
为了解决以上各种问题,同时也为了让MiniGUI更加适合于嵌入式系统,我们开发了MiniGUILite 版本。
2Lite版本简介
在MiniGUILite 版本中,我们可以同时运行多个MiniGUI应用程序。首先我们启动一个服务器程序mginit,然后我们可以从中启动其他做为客户运行的MiniGUI应用程序。如果因为某种原因客户终止,服务器可以继续运行。mginit程序建立了一个虚拟控制台窗口。我们可以从这个虚拟控制台的命令行启动其他的程序,甚至可以通过gdb调试这些程序。这大大方便了MiniGUI应用程序的调试。
MiniGUI-Lite区别于MiniGUI原有版本的最大不同在于我们可以在MiniGUI-Lite程序中创建多个窗口,但不能启动新的线程建立窗口。除此之外,其他几乎所有的API都和MiniGUI原有版本是兼容的。因此,从MiniGUI原有版本向MiniGUI-Lite版本的移植是非常简单的。象mglite-exec包中的程序,其中所有的程序均来自miniguiexec包,而每个源文件的改动不超过5行。
3Lite版本的设计
设计之初,我们确定MiniGUILite 版本的开发目的:
·保持与原先MiniGUI版本在源代码级98%以上的兼容。
·不再使用LinuxThreads。
·可以同时运行多个基于MiniGUILite 的应用程序,即多个进程,并且提供前后台进程的切换。
显然,要满足这三个设计目的,如果采用传统的C/S结构对现有MiniGUI进行改造,应该不难实现。但传统C/S结构的缺陷却无法避免。经过对PDA等嵌入式系统的分析,我们发现,某些PDA产品具有运行多个任务的能力,但同一时刻在屏幕上进行绘制的程序,一般不会超过两个。因此,只要确保将这两个进程的绘制相互隔离,就不需要采用复杂的C/S结构处理多个进程窗口之间的互相剪切。在这种产品中,如果采用基于传统C/S结构的多窗口系统,实际是一种浪费。
因此,我们对MiniGUI-Lite版本进行了如下简化设计:
每个进程维护自己的主窗口Z序,同一进程创建的主窗口之间互相剪切。也就是说,除了只有一个线程,只有一个消息循环之外,一个进程与原有的MiniGUI版本之间没有任何区别。每个进程在进行屏幕绘制时,不需要考虑其他进程。
建立一个简单的客户/服务器体系,但确保最小化进程间的数据复制功能。因此,在服务器和客户之间传递的数据仅限于输入设备的输入数据,以及客户和服务器之间的某些请求和响应数据。
有一个服务器进程(mginit),它负责初始化一些输入设备,并且通过UNIXDomain 套接字将输入设备的消息发送到前台的MiniGUILite 客户进程。
服务器和客户被分别限定在屏幕的某两个不相交矩形内进行绘制,同一时刻,只能有一个客户及服务器进行屏幕绘制。其他客户可继续运行,但屏幕输入被屏蔽。服务器可以利用API接口将某个客户切换到前台。同时,服务器和客户之间采用信号和SystemV 信号量进行同步。
服务器还采用SystemV IPC 机制提供一些资源的共享,包括位图、图标、鼠标、字体等等,以便减少实际内存的消耗。
4Lite版本的一些实现细节
4.1系统初始化
应用程序的入口点为main()函数,而MiniGUI应用程序的入口点为MiniGUIMain,在这两个入口点之间,是MiniGUI的初始化部分和结束部分。
在系统初始化时,MiniGUI区分两种情况:服务器(Server)和客户(Client)。针对这两种不同的情况,随后的各项操作均有不同的处理,这主要依据全局变量mgServer。由于仅仅根据名称判断是否为服务器,所以服务器的名字只能是"mginit"。InitGUI()是对MiniGUI进行初始化的函数,它主要负责:
获取有关终端的信息。
初始化图形抽象层。
如果是服务器,则装入共享资源,若为客户则与共享资源建立连接。
建立与窗口活动有关的运行环境。
如果为服务器,则初始化事件驱动抽象层(IAL),如果为客户,则打开与服务器事件驱动器的通道。
如果为服务器,则设定空闲处理为IdleHandler4Server,如果为客户,则设定空闲处理为IdleHandle4Client。
流程如图2(为突出重点,我们忽略了一些细节):
4.2共享资源初始化
共享资源是客户服务器模型中的重要元素,它由服务器负责创建和释放,而提供所有客户程序共享的数据资源。它的初始化过程由图3所示的调用流完成。
如果是服务器,则初始化此结构,src/kernel/sharedres.c/LoadSharedResource()负责完成这一任务
对于客户,则只需要与此结构进行连接即可,它在src/kernel/sharedres.c/AttachSharedResource()实现
4.3服务器客户通信连接初始化
在客户服务器模型的讨论中,我们还将详细的讨论服务器客户的通信机制,这里只给出初始化的调用关系。
4.4多进程模型
Lite版本是支持客户服务器(C/S)方式的多进程系统,在运行过程中有且仅有一个服务器程序在运行,它的全局变量mgServer被设为TRUE,其余的MiniGUI应用程序为客户,mgServer变量被设为FALSE。各个应用程序分别运行于各自不同的进程空间
目前的程序结构使每个加载的进程拥有一个自已的桌面模型及其消息队列,进程间的通信依靠以下所提到的进程通信模型来完成。
4.5进程通信模型
这里我们所指的进程通信包括通过共享内存实现的数据交换和通过套接字实现的客户服务器通信模型。
服务器负责装入共享资源,其中包括系统图标、位图、字体等,客户则通过AttachSharedResource()获取指向共享资源的指针,初始化一块共享内存及与使用已有共享内存的方法在前面的描述中已提到,在此不再赘述。
4.6各进程之间的同步
这里所指的进程同步主要是指各进程绘制的同步,显然,同时不可能有两个进程向屏幕绘制。传统的GUI实现大多是只有一个进程负责绘制,而在我们Lite版本中,各进程负责自己的绘制。同时,我们的Lite版本还支持虚屏切换,当我们切换出去的时候,谁也不能够向屏幕绘制。
Lite版本利用Unix信号解决了绘制同步问题。系统定义了两个信号:SIG_SETSCR和SIG_DNTDRAW,它们其实是重定义了的信号SIGUNUSED和SIGSTKFLT。每个进程都定义了两个变量dont_draw和cant_draw。
服务器利用SIG_SETSCR和SIG_DNTDRAW来控制各客户程序谁有权对屏幕绘制,而不是自己全权代理。这也使得进程间通信量大大减少:当服务器希望一个客户程序不要向屏幕绘制时,就向它发送SIG_DNTDRAW信号,当让其绘制时,则发送SIG_SETSCR。从而实现了各进程间的屏幕绘制同步。
当一个客户收到SIG_DNTDRAW时,将自己的变量dont_daw设置为ture,收到SIG_SETSCR时,则将dont_draw变量设置为false。另一个变量cant_draw则是给客户自己用的,比如,做剪切时,当它的剪切域为空集时,又比如,当进行虚屏切换时,当前的进程将自己的cant_draw变量设置为true。
另外,如果一个客户正在绘图,我们只有等它画完后才能让其他进程得到这一权利。我们不需要知道谁在绘图,但我们要等到这一过程结束。Lite版本利用信号量机制解决了这一问题。在共享内存里保存着一个变量shmid,各进程利用它来实现各自的锁机制。这种机制有点类似于文件锁,不过要快许多。
从而,利用信号量机制,Lite版本实现了多进程的绘制同步。服务器利用信号控制各客户,而各客户也充分合作。相关代码都在MiniGUI的系统库里实现。保证了系统的稳定运行。
5总结语
MiniGUILite版本试图在传统的基于线程的单体结构和C/S结构之间寻求一种效率和稳定性的折中,以便更加适合运行在PDA等小型嵌入式系统中。如今,MiniGUILite版本已经稳定地运行在一些PDA系统上,事实证明这种尝试是成功的。
安装手册
MiniGUI简易安装手册
魏永明(2001/08/09)
1.安装前的准备
1)选择图形引擎
如果您的Linux内核支持FrameBuffer,则可以使用内建于MiniGUI的图形引擎--即"私有引擎".这样,就没有必要安装下面提到的其它图形函数库作为MiniGUI的图形引擎了.而且,Native 引擎是唯一支持MiniGUI-Lite的引擎,如果您要将MiniGUI配置为MiniGUI-Lite,也没有必要安装其它的图形库作为引擎.
Native引擎既可以支持MiniGUI-Threads,也可以支持MiniGUI-Lite.需要注意的是,Native 引擎目前还不能提供对"fbvga16"(标准VGA16 色模式)的良好支持.而且需要注意Native引擎目前只能运行在LinuxFrameBuffer 之上.
如果您的Linux内核不支持FrameBuffer,则可以使用SVGALib作为图形引擎.SVGALib 是一种直接访问图形芯片的老的图形函数库.因此,如果使用SVGALib,则需要获得root权限才能让MiniGUI程序支持进行图形芯片的I/O操作.
需要注意的是,因为SVGALib是一种老的图形库,所以不能对大部分流行的显示卡提供良好支持.然而,如果您的显示芯片是VESA兼容的,则SVGALib可以通过VESABIOS 2.0 提供的接口良好运行.许多显示芯片都是VESA兼容的,但某些不是,比如Intel的i810芯片组.
你也可以使用LibGGI作为MiniGUI的图形引擎.LibGGI 是一种新的面向Linux的图形引擎,它也可以稳定地运行在LinuxFrameBuffer 之上.最主要的是,运行在LibGGI之上的应用程序可以方便地运行在XWindow 之上,而且不需要重新编译.
注意,SVGALib 和LibGGI不能用来支持MiniGUI-Lite.
2)安装引擎
如果您决定使用Native引擎,则没有必要预先安装某个特定的图形库.Native 引擎已经内建于MiniGUI当中.
如果要使用SVGALib作为图形引擎,则需要安装经过修改的"svgalib-1.4.0-hz"软件包和"vgagl4-0.1.0"包.请从如下FTP站点下载:
ftp://ftp.minigui.net/pub/minigui/dep-libs
或者从HTTP站点下载:
http://www.minigui.org/cdownload.shtml
并将上述两个软件包安装到您的系统当中.安装过程将覆盖系统中老的SVGALib,但并不会影响您的系统.
如果您使用LibGGI作为图形引擎,请从http://www.ggi-projects.org下载最新的GGI源代码,并安装之.或者,也可以从我们的FTP站点上下载:
ftp://ftp.minigui.net/pub/minigui/dep-libs
或者从HTTP站点下载:
http://www.minigui.org/cdownload.shtml
3)下载MiniGUI
在下载并安装好图形引擎之后,请从我们的站点上下载如下tar.gz软件包并解开:
libminigui-1.0.xx.tar.gz:MiniGUI 函数库源代码,其中包括libminigui,libmywins, libmgext, 和libvcongui.
minigui-res-1.0.xx.tar.gz:MiniGUI 所使用的资源,包括图标,位图和鼠标光标.
minigui-fonts-1.0.xx.tar.gz:MiniGUI 所使用的基本字体.在我们的站点上,还可以找到其它字体.
minigui-imetabs-1.0.xx.tar.gz:中文GB输入法所使用的码表.
minigui-exec-1.0.xx.tar.gz:MiniGUI-Threads 的示例程序.
mglite-exec-1.0.xx.tar.gz:MiniGUI-Lite 的示例程序.
注意,如果你以前安装过"minigui-res","minigui-fonts", 或者"minigui-imetabs"的老版本,而且站点上并没有提供新版本的话,说明您可以继续沿用老的版本.
2安装MiniGUI的资源文件
我们首先要安装MiniGUI的资源文件.请按照如下步骤:
1)使用"tar"命令解开"minigui-res-1.0.xx.tar.gz".可使用如下命令:
$tar zxf minigui-res-1.0.xx.tar.gz
2)改变到新建目录中,然后以超级用户身份运行"make"命令:
$su -c make install
3)使用相同的步骤安装"minigui-fonts"和"minigui-imetabs"软件包.
3配置和编译MiniGUI
MiniGUI 使用了"automake"和"autoconf"接口,因而MiniGUI的配置和编译非常容易:
1)使用"tar"解开"libminigui-1.0.xx.tar.gz"到新的目录:
$tar zxf libminigui-1.0.xx.tar.gz
2)改变到新目录,然后运行"./configure":
$./configure
3)运行下面的命令编译并安装MiniGUI:
$make; su -c 'make install';
4)默认情况下,MiniGUI 的函数库将安装在`/usr/local/lib'目录中.您应该确保该目录已经列在"/etc/ld.so.conf"文件中.并且在安装之后,要运行下面的命令更新共享函数库系统的缓存:
$su -c ldconfig
5)如果要控制您的MiniGUI提供那些功能,则可以运行:
$./configure --help
查看完整的配置选项清单,然后通过命令行开关打开或者关闭某些功能.例如,如果您不希望MiniGUI使用LoadBitmap函数装载JPEG图片,则可以使用:
$./configure --disable-jpgsupport
6)注意,某些MiniGUI特色依赖于其它函数库,请确保已安装了这些函数库.
4运行MiniGUI的演示程序
"minigui-exec-1.0.xx.tar.gz" 和"mglite-exec-1.0.xx.tar.gz"中分别包含了MiniGUI-Threads和MiniGUI-Lite的演示程序.如果将MiniGUI配置为MiniGUI-Threads,则请编译并运行minigui-exec软件包,否则,请编译并运行mglite-exec软件包.
运行之前,应该解开并编译这些tar.gz包:
1)使用"tar"命令将软件包解开到新的目录.
2)依次运行"./configure"和"make"编译演示程序.
3)运行"makeinstall" 将安装其中的一些应用程序到系统中.注意这些程序将被安装到"/usr/local/bin"目录中,而某些演示程序并不会安装到系统当中.
4)尝试运行演示程序和应用程序.例如,如果将MiniGUI配置为MiniGUI-Threads,则可以进入"minigui-exec-1.0.xx/amuze/"运行"./amuze".
5)如果配置并安装了MiniGUI-Lite,则应该首先运行服务器,然后从服务器当中运行其它演示程序.编译"mglite-exec"将生成一个"mginit"程序,该程序将提供一个运行于MiniGUI-Lite的虚拟控制台.
5.安装及配置示例
本示例假定用户使用的系统是RedHatLinux 6.x 发行版,使用Linux内核2.2.xx或者2.4.xx,用户的目标是运行MiniGUI-Lite(使用MiniGUIVersion 1.0.00). 步骤如下:
1)确保您的PC机显示卡是VESA兼容的.大多数显示卡是VESA兼容的,然而某些内嵌在主板上的显示卡可能不是VESA兼容的,比如Inteli810 系列.如果显示卡是VESA兼容的,就可以使用Linux内核中的VESAFrameBuffer 驱动程序了.
2)确保您的Linux内核包含了FrameBuffer支持,并包含了VESAFrameBuffer 驱动程序.RedHat Linux 6.x 发行版自带的内核中已经包含了该驱动程序.如果使用自己编译的内核,请检查您的内核配置.
3)修改/etc/lilo.conf文件,在您所使用的内核选项段中,添加如下一行:
vga=0x0317
这样,Linux 内核在启动时将把显示模式设置为1024x768x16bpp模式.如果您的显示器无法达到这种显示分辨率,可考虑设置vga=0x0314,对应800x600x16bpp模式.
修改后的/etc/lilo.conf文件类似:
boot=/dev/hda
map=/boot/map
install=/boot/boot.b
prompt
timeout=50
linear
default=linux
image=/boot/vmlinuz-2.4.2
vga=0x0317 ;这一行设置显示模式.
label=linux
read-only
root=/dev/hda6
other=/dev/hda1
label=dos
4)运行lilo命令,使所作的修改生效,并重新启动系统.
5)如果一切正常,将在Linux内核的引导过程中看到屏幕左上角出现可爱的Linux吉祥物--企鹅,并发现系统的显示模式发生变化.
6)按照第4节所讲,下载libminigui-1.0.00.tar.gz,mglite-exec-1.0.02.tar.gz, 以及minigui-res-1.0.02.tar.gz,minigui-fonts-1.0.00.tar.gz, minigui-imetabs-0.9.96.tar.gz 等软件包.注意要安装正确的版本.
7)以root用户身份安装minigui-res-1.0.02.tar.gz,minigui-fonts-1.0.00.tar.gz, 和minigui-imetabs-0.9.96.tar.gz软件包.这些软件包的安装一般不会出现问题.此处不再赘述.
8)在某个目录下解开libminigui-1.0.00.tar.gz,并进入新建的libminigui-1.0.00目录.
$tar zxf libminigui-1.0.00.tar.gz
$ cd libminigui-1.0.00
9)依次运行如下命令:
$./autogen.sh ; 如果您的系统中没有安装autoconf/automake,
; 则不要执行这一步.
$ ./configure
$ make
10)以root身份运行makeinstall 命令:
$su -
# make install
11)修改/etc/ld.so.conf文件,将/usr/local/lib目录添加到该文件最后一行.修改后类似:
/usr/lib
/usr/X11R6/lib
/usr/i486-linux-libc5/lib
/usr/local/lib
12)以root身份执行ldconfig命令:
#ldconfig
13)在新目录中解开mglite-exec-1.0.02.tar.gz,并进入新建目录:
$tar zxf mglite-exec-1.0.02.tar.gz
$ cd mglite-exec-1.0.02
14)依次运行如下命令:
$./autogen.sh ; 如果您的系统中没有安装autoconf/automake,
; 则不要执行这一步.
$ ./configure
$ make
15)进入mginit目录,并执行mginit程序:
$cd mginit
$ ./mginit
16)如果一切正常,这时可以看到一个虚拟控制台出现在屏幕上.
17)切换到../demos目录,执行其中的程序:
$cd ../demos
$ ./fminigui
18)如果能够在屏幕上看到一个不断飞动的GUI窗口,则表明一切OK.
19)如何关闭这个窗口,不需要在这里赘述了吧.:)
[本文件由良友·收藏家自动生成]