| MiniGUI |
·体系结构概览(本文)。将在整体上对 MiniGUI 1.0 的体系结构进行介绍。重点包括:线程的基本概念;多线程的微客户/服务器体系、多线程通讯的关键数据结构――消息队列;面向对象技术在 MiniGUI 中的应用等等。
·MiniGUI 的多窗口管理。将介绍 MiniGUI 的多窗口机制以及相关的窗口类技术。其中涉及到窗口剪切处理和 Z 序,消息传递,控件类设计和输入法模块设计等等。
·MiniGUI 的图形设备管理。重点介绍 MiniGUI 是如何处理窗口绘制的。其中主要包括图形上下文的概念,坐标映射,图形上下文的局部、全局和有效剪切域的概念等等。
·图形抽象层和输入抽象层。图形抽象层(GAL)和输入抽象层(IAL)大大提高了 MiniGUI 的可移植性,并将底层图形设备和上层接口分离开来。这里将重点介绍 MiniGUI 的 GAL 和 IAL 接口,并以 EP7211 等嵌入式系统为例,说明如何将 MiniGUI 移植到新的嵌入式平台上。
·多字体和多字符集支持。MiniGUI 采用逻辑字体实现多字体和多字符集处理。这一技术成功应用了面向对象技术,通过单一的逻辑接口,可以实现对各种字符集以及各种字体的支持。
MiniGUI 是一个基于线程的窗口系统。为了理解 MiniGUI 的体系结构,我们有必要首先对线程作一番了解。
2.1 什么是线程
线程通常被定义为一个进程中代码的不同执行路线。也就是说,一个进程中,可以有多个不同的代码路线在同时执行。例如,常见的字处理程序中,主线程处理用户输入,而其他并行运行的线程在必要时可在后台保存用户的文档。我们也可以说线程是“轻量级进程”。在 Linux 中,每个进程由五个基本的部分组成:代码、数据、栈、文件I/O 和信号表。因此,系统对进程的处理要花费更多的开支,尤其在进行进程调度和任务切换时。从这个意义上,我们可以将一般的进程理解为重量级进程。在重量级进程之间,如果需要共享信息,一般只能采用管道或者共享内存的方式实现。如果重量级进程通过 fork() 派生了子进程,则父子进程之间只有代码是共享的。
而我们这里提到的线程,则通过共享一些基本部分而减轻了部分系统开支。通过共享这些基本组成部分,可以大大提高任务切换效率,同时数据的共享也不再困难――因为几乎所有的东西都可以共享。
从实现方式上划分,线程有两种类型:“用户级线程”和“内核级线程”。
用户线程指不需要内核支持而在用户程序中实现的线程,这种线程甚至在象 DOS 这样的操作系统中也可实现,但线程的调度需要用户程序完成,这有些类似 Windows 3.x 的协作式多任务。另外一种则需要内核的参与,由内核完成线程的调度。这两种模型各有其好处和缺点。用户线程不需要额外的内核开支,但是当一个线程因 I/O 而处于等待状态时,整个进程就会被调度程序切换为等待状态,其他线程得不到运行的机会;而内核线程则没有各个限制,但却占用了更多的系统开支。
Linux 支持内核级的多线程,同时,也可以从 Internet 上下载一些 Linux 上的用户级的线程库。Linux 的内核线程和其他操作系统的内核实现不同,前者更好一些。大多数操作系统单独定义线程,从而增加了内核和调度程序的复杂性;而 Linux 则将线程定义为“执行上下文”,它实际只是进程的另外一个执行上下文而已。这样,Linux 内核只需区分进程,只需要一个进程/线程数组,而调度程序仍然是进程的调度程序。Linux 的 clone 系统调用可用来建立新的线程。
2.2 POSIX 线程
POSIX 标准定义了线程操作的 C 语言接口。我们可以将 POSIX 线程的接口划分如下:
·线程的建立和销毁。用来创建线程,取消线程,制造线程取消点等等。
·互斥量操作接口。提供基本的共享对象互斥访问机制。
·信号量操作接口。提供基本的基于信号量的同步机制。不能与 System V 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;
}
int main (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 with semaphores.
All integers between 0 and 9999 should be printed exactly twice,
once to the right of the arrow and once to the left. */
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#define BUFFER_SIZE 16
/* Circular buffer of integers. */
struct prodcons {
int buffer[BUFFER_SIZE]; /* 实际数据 */
int readpos, writepos; /* 读取和写入的位置 */
sem_t sem_read; /* 可读取的元素个数 */
sem_t sem_write; /* 可写入的空位个数 */
};
/* 初始化缓冲区 */
void init(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;
}
/* 在缓冲区中保存一个整数 */
void put(struct prodcons * b, int data)
{
/* Wait until buffer 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);
}
/* 从缓冲区读取并删除数据 */
int get(struct prodcons * b)
{
int data;
/* Wait until 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 的整数,另一个线程读取并打印。*/
#define OVER (-1)
struct prodcons 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;
}
int main(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 操作。
通过上面的两个例子,读者可以对线程之间的互操作有一个大概了解。如果读者对 System V IPC 机制比较熟悉的话,也可以作一番比较。可以看到,多线程的最大好处是,除堆栈之外,几乎所有的数据均是共享的,因此线程间的通讯效率最高;但最大坏处是,因为共享所有数据,从而非常容易导致线程之间互相破坏数据。
2.3 MiniGUI 和多线程
MiniGUI 1.0 版本采用了多线程机制,也就是说,MiniGUI 以及运行在 MiniGUI 之上的所有应用程序均运行在同一个地址空间之内。比起其他基于进程的 GUI 系统来说,虽然缺少了地址保护,但运行效率却是最高的。
3.1 多线程的分层设计
从整体结构上看,MiniGUI 是分层设计的,层次结构见图 1。在最底层,GAL 和 IAL 提供底层图形接口以及鼠标和键盘的驱动;中间层是 MiniGUI 的核心层,其中包括了窗口系统必不可少的各个模块;最顶层是 API,即编程接口。
GAL 和 IAL 为 MiniGUI 提供了底层的 Linux 控制台或者 X Window 上的图形接口以及输入接口,而 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.1 消息和消息循环
在任何 GUI 系统中,均有事件或消息驱动的概念。在MiniGUI中,我们使用消息驱动作为应用程序的创建构架。
在消息驱动的应用程序中,计算机外设发生的事件,例如键盘键的敲击、鼠标键的按击等,都由支持系统收集,将其以事先的约定格式翻译为特定的消息。应用程序一般包含有自己的消息队列,系统将消息发送到应用程序的消息队列中。应用程序可以建立一个循环,在这个循环中读取消息并处理消息,直到特定的消息传来为止。这样的循环称为消息循环。一般地,消息由代表消息的一个整型数和消息的附加参数组成。
应用程序一般要提供一个处理消息的标准函数。在消息循环中,系统可以调用此函数,应用程序在此函数中处理相应的消息。
MiniGUI 支持如下几种消息的传递机制。这些机制为多线程环境下的窗口间通讯提供了基本途径:
·通过 PostMessage 发送。消息发送到消息队列后立即返回。这种发送方式称为“邮寄”消息。如果消息队列中的邮寄消息缓冲区已满,则该函数返回错误值。
·通过 PostSyncMessage 发送。该函数用来向不同于调用该函数的线程消息队列邮寄消息,并且只有该消息被处理之后,该函数才能返回,因此这种消息称为“同步消息”。
·通过 SendMessage 发送。该函数可以向任意一个窗口发送消息,消息处理完成之后,该函数返回。如果目标窗口所在线程和调用线程是同一个线程,该函数直接调用窗口过程,如果处于不同的线程,则利用 PostSyncMessage 函数发送同步消息。
·通过 SendNotifyMessage 发送。该函数向指定的窗口发送通知消息,将消息放入消息队列后立即返回。由于这种消息和邮寄消息不同,是不允许丢失的,因此,系统以链表的形式处理这种消息。
通过 SendAsyncMessage 发送。利用该函数发送的消息称为“异步消息”,系统直接调用目标窗口的窗口过程。
读者可以联系我们在第1节中给出的“生产者/消费者”问题而想到一个简单的消息队列的实现,该消息队列可以简单地设计为一个类似清单 2 的循环队列。但是,GUI 系统中的消息队列并不能是一个简单的循环队列,它还要注意到如下一些问题:
消息一般附带有相关的数据,这些数据对各种消息具有不同的含义,在多窗口环境,尤其是多进程环境下,消息数据的有效传递非常重要。
消息作为窗口间进行数据交换的一种方式,要提供多种传递机制。某些情况下,发送消息的窗口要等到这个消息处理完成之后,知道处理的结果之后才能继续执行;而有些情况下,发送消息的窗口只是简单地向接收消息的窗口通知某些事件的发生,一般发送出消息之后就返回。后一种情况类似于邮寄信件,所以通常称为邮寄消息。更有一种较为复杂的情况,就是等待一个可能长时间无法被处理的消息时,发送的消息的窗口设置一个超时值,以便能够在消息得不到及时处理的情况下能够恢复执行。
某些特殊消息的处理也需要注意,比如定时器。当某个定时器的频率很高,而处理这个定时器的窗口的反应速度又很慢,这时如果采用邮寄消息或者发送消息的方式,线性的循环队列最终就会塞满。
最后一个问题是消息优先级的问题。一般情况下,要考虑优先处理鼠标或键盘的输入消息,其次才是重绘和定时器等消息。
特殊消息的处理。由于窗口重绘消息的特殊性(通常比较花费时间),只有当程序将其他消息处理之后,才会处理重绘消息。并且只有存在窗口的无效区域的时候,才会通知程序处理窗口的重绘。
鉴于以上要特殊考虑的问题,MiniGUI 中的消息队列要比清单 2 中的循环队列复杂。参见清单 3。
清单 3 MiniGUI 的消息队列定义
typedef struct _MSGQUEUE
{
DWORD dwState; // 消息队列状态
pthread_mutex_t lock; // 互斥锁
sem_t wait; // 等待信号量
PQMSG pFirstNotifyMsg; // 通知消息队列的头
PQMSG pLastNotifyMsg; // 通知消息队列的尾
PSYNCMSG pFirstSyncMsg; // 同步消息队列的头
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 结构的定义也很简单:
typedef struct _QMSG
{
MSG Msg;
struct _QMSG* next;
BOOL fromheap;
}QMSG;
typedef QMSG* PQMSG;
用于同步消息传递的数据结构为 SYNCMSG,该结构在消息队列中也形成了一个链表,但该结构本身稍微复杂一些:
typedef struct _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 个定时器是否到期。消息队列中同时还有三个成员:
HWND TimerOwner[8]; // 定时器所有者
int TimerID[8]; // 定时器标识符
BYTE TimerMask; // 已使用的定时器掩码
其中 TimerMask 表示当前有效的定时器,每位表示一个定时器;TimerID 表示这 8 个定时器的标识符(整数);而 TimerOwner 则表示定时器的所有者(窗口句柄)。这种定时器的实现方法类似 Linux 内核中的信号实现。定时器是否有效以及是否到期均由二进制字节的一个位来表示。当 GetMessage 检查这些标志时发现有某个定时器到期才会获得一个定时器消息。也就是说,定时器消息是不排队的。这样就解决了排队时可能塞满消息队列的问题。
5.1 控件类和控件
MiniGUI 中的每个控件都属于某种子窗口类,是对应子窗口类的实例。这类似于面向对象技术中类和对象的关系。
每个控件的消息实际都是有该控件所属控件类的回调函数处理的,从而可以让每个属于统一控件类的控件均保持有相同的用户界面和处理行为。
但是,如果我们在调用某个控件类的回调函数之前,首先调用自己定义的某个回调函数的话,我们就可以让该控件重载控件类的某些处理行为,从而让该控件一方面继承控件类的大部分处理行为,另一方面又具有自己的特殊行为。这实际就是面向对象中的继承和派生。比如,一般的编辑框会接收所有的键盘输入,当我们希望自己的编辑框只接收数字时,就可以用这种办法屏蔽非数字的字符输入。
5.2 GAL 和 IAL
在 MiniGUI 0.3.xx 的开发中,我们引入了图形和输入抽象层(Graphics and Input Abstract Layer,GAL 和 IAL)的概念。抽象层的概念类似 Linux 内核虚拟文件系统的概念。它定义了一组不依赖于任何特殊硬件的抽象接口,所有顶层的图形操作和输入处理都建立在抽象接口之上。而用于实现这一抽象接口的底层代码称为“图形引擎”或“输入引擎”,类似操作系统中的驱动程序。这实际是一种面向对象的程序结构。利用 GAL 和 IAL,MiniGUI 可以在许多图形引擎上运行,比如 SVGALib 和 LibGGI,并且可以非常方便地将 MiniGUI 移植到其他 POSIX 系统上,只需要根据我们的抽象层接口实现新的图形引擎即可。目前,我们已经编写了基于 SVGALib 和 LibGGI 的图形引擎。利用 LibGGI, MiniGUI 应用程序可以运行在 X Window 上,将大大方便应用程序的调试。我们目前正在进行 MiniGUI 私有图形引擎的设计开发。通过 MiniGUI 的私有图形引擎,我们可以最大程度地针对窗口系统对图形引擎进行优化,最终提高系统的图形性能和效率。
GAL 和 IAL 的结构是一样的,我们这里只拿 GAL 作为实例说明面向对象技术的运用,参见图 4。
系统维护一个已注册图形引擎数组,保存每个图形引擎数据结构的指针。系统利用一个指针保存当前使用的图形引擎。一般而言,系统中至少有两个图形引擎,一个是“哑”图形引擎,不进行任何实际的图形输出;一个是实际要使用的图形引擎,比如 LibGGI 或者 SVGALib。每个图形引擎的数据结构定义了该图形引擎的一些信息,比如标识符、属性等,更重要的是,它实现了 GAL 所定义的各个接口,包括初始化和终止、图形上下文管理、画点处理函数、画线处理函数、矩形框填充函数、调色板函数等等。
如果在某个实际项目中所使用的图形硬件比较特殊,现有的图形引擎均不支持。这时,我们就可以安照 GAL 所定义的接口实现自己的图形引擎,并指定 MiniGUI 使用这种私有的图形引擎即可。这种软件技术实际就是面向对象多态性的具体体现。
利用 GAL 和 IAL,大大提高了 MiniGUI 的可移植性,并且使得程序的开发和调试变得更加容易。我们可以在 X Window 上开发和调试自己的 MiniGUI 程序,通过重新编译就可以让 MiniGUI 应用程序运行在特殊的嵌入式硬件平台上。
5.3 字符集和字体支持
在成功引入 GAL 和 IAL 之后,我们又在处理字体和字符集的模块当中引入了逻辑字体的概念。逻辑字体是 MiniGUI 用来处理文本(包括文本输出和文本分析)的顶层接口。逻辑字体接口将各种不同的字体(比如宋体、黑体和揩体)和字体格式(比如等宽字体、变宽字体等光栅字体和 TrueType 等矢量字体),以及各种不同字符集(ISO-8859、GB2312、Big5、UNICODE等)综合了起来,从而可以通过统一的接口显示不同字符集的不同字体的文本,并且还可以分析各种字符集文本的组成,比如字符、单词等。在多字体和多字符集的支持中,我们也采用了面向对象的软件技术,使得添加新的字体支持和新的字符集支持非常方便。目前,MiniGUI 能够支持各种光栅字体和 TrueType、Adobe Type 1 等矢量字体,并能够支持 GB2312、Big5 等多字节字符集,UNICODE 的支持正在开发当中。
相对 GAL 和 IAL 而言,MiniGUI 中的字符集和字体支持更加复杂,涉及到的内容也较多。前面提到,我们通过逻辑字体这一接口,实现了文字输出和文本分析两个功能。实际这两个功能是相互关联的。在进行文本输出时,尤其在处理多字节字符集,比如 GB2312 或者 Big5 时,首先要对文本进行分析,以便判断是否是一个属于该字符集的双字节字符。
尽管 MiniGUI 采用多线程机制实现了一个小巧、高效的窗口系统,但有很多理由希望 MiniGUI 能够采用多进程机制实现(尽管多进程机制可能带来通讯上的额外开支):
·良好的地址保护。窗口本身的崩溃不会影响 MiniGUI 的运行,而目前的多线程机制无法提供地址保护。
·信号处理上的问题。在多线程程序中,所有的多线程共享同一个信号处理方式,包括是否忽略、是否捕获等等。这对某些大型软件是很难接受的。
·多线程程序对程序员要求较高。在编写多线程程序时,通常要考虑到函数的“线程安全”问题,即函数是否是可重入的,因此,我们通常不能使用全局或者静态变量。
鉴于上述需求,我们将在接下来的 MiniGUI 2.0 开发中,进行一些体系结构上的调整,其中最为重要的就是采用进程机制替代线程机制。
在任何一个足够复杂的 GUI 系统中,处理窗口之间的互相剪切是其首要解决的问题。因为多窗口系统首先要确保一个窗口中的绘制输出不会影响到另外一个窗口。为此,GUI 系统一般要利用 Z 序来管理窗口之间的互相剪切关系。根据窗口在 Z 序中所处的位置,GUI 系统要计算每个窗口受剪切的区域,即剪切域。通常,窗口的剪切域定义为互不相交的矩形集合。GUI 系统的底层图形引擎在进行输出时,要根据当前输出的剪切域进行输出的剪切操作。从而保证窗口的绘制输出不会互相影响。因为任何一个窗口的创建、销毁、隐藏、显示均有可能影响其他窗口的剪切域,所以首先要有一个高效的剪切域维护算法。本文将详细描述 MiniGUI 中的剪切域生成算法。
许多人对控件(或者部件)的概念已经相当熟悉了。控件可以理解为主窗口中的子窗口。这些子窗口的行为和主窗口一样,即能够接收键盘和鼠标等外部输入,也可以在自己的区域内进行输出――只是它们的所有活动被限制在主窗口中。MiniGUI 也支持子窗口,并且可以在子窗口中嵌套建立子窗口。我们将 MiniGUI 中的所有子窗口均称为控件。
在 Windows 或 X Window 中,系统会预先定义一些控件类,当利用某个控件类创建控件之后,所有属于这个控件类的控件均会具有相同的行为和显示。利用这些技术,可以确保一致的人机操作界面,而对程序员来讲,可以像搭积木一样地组建图形用户界面。MiniGUI 使用了控件类和控件的概念,并且可以方便地对已有控件进行重载,使得其有一些特殊效果。比如,需要建立一个只允许输入数字的编辑框时,就可以通过重载已有编辑框而实现,而不需要重新编写一个新的控件类。
在多语种环境中,输入法是一个必不可少的模块。输入法提供了将标准键盘输入翻译为适当语种的文字的能力。MiniGUI 中也包含有标准的中文简体输入法,包括全拼、五笔和智能拼音等等。本文最后将介绍 MiniGUI 中的输入法模块实现。
Z 序实际定义了窗口之间的层叠顺序。说起“Z 序”这个名称,实际是相对屏幕坐标而言的。一般而言,屏幕上的所有窗口均有一个坐标系,即原点在左上角,X 轴水平向右,Y 轴垂直向下的坐标系。Z 序就是相对于一个假想的 Z 轴而言的,这个 Z 轴从屏幕外指向屏幕内。窗口在这个 Z 轴上的值,就确定了其 Z 序。Z 序值大的窗口,覆盖了 Z 序值小的窗口。
当然,在程序当中,Z 序一般表示为一个链表。越接近于链表头的节点,其 Z 序值就越大。在 MiniGUI 中,我们维护了两个 Z 序。其中一个 Z 序永远位于另一个 Z 序之上。这样,就可以创建始终位于其他窗口之上的窗口,比如输入法窗口。如果在建立窗口时,指定了 WS_EX_TOPMOST 扩展属性,就可以创建这样的主窗口。因为 Z 序的操作实际就是链表的操作,这里就不再赘述。
有了窗口 Z 序,我们就可以计算每个窗口的剪切域。我们把因为窗口 Z 序而产生的剪切域称为“全局剪切域”,这是相对于窗口自身定义的剪切域而言的,我们把后者称为“局部剪切域”。窗口中的所有输出,首先要受到全局剪切域的影响,其次受到局部剪切域的影响。我们在这里重点讲解窗口的全局剪切域的生成和维护。
3.1 全局剪切域的生成和维护
在 MiniGUI 中,剪切域表示为若干互不相交的矩形之并集,这些矩形称为剪切矩形。最初,屏幕上没有任何窗口时,桌面的剪切域由一个矩形组成,即屏幕矩形;当屏幕上只有一个窗口时,该窗口的剪切域由一个矩形组成,该矩形即为窗口在屏幕上的矩形,而桌面的剪切域却可能是由多个矩形组成的。
读者很容易看出,在只有一个窗口的情况下,形成桌面剪切域的矩形最多只能有四个。
此时,如果有一个新的窗口出现,则新的窗口将同时剪切旧的窗口和桌面(图 3。窗口的剪切矩形用空心矩形表示,而桌面的剪切矩形用实心矩形表示)。而这时,桌面和旧窗口的剪切域将多出一些矩形,这些矩形应该是原有剪切域中的每个矩形受到新窗口矩形影响之后生成的剪切矩形。同样,原有剪切域中的每个矩形只能最多只能派生出4个新剪切域,而某些矩形根本不会受到新窗口矩形的影响。
这样,我们可以将某个窗口全局剪切域归纳为原有剪切域中排除(Exclude)某个矩形而生成的:
窗口的全局剪切域初始化为窗口矩形。
当窗口之上有其他窗口覆盖时,则该窗口的全局剪切域为排除新窗口矩形之后的剪切域。
沿 Z 序迭代第 2 步,直到最顶层窗口。
清单 1 中的代码是在显示一个新窗口时,MiniGUI 处理被该窗口所覆盖的其他所有窗口的代码。这段代码调用了剪切域维护接口中的 SubtractClipRect 函数计算新的剪切域。
清单 1 显示新窗口时计算被新窗口覆盖的窗口的全局剪切域
// clip all windows under this window.
static void clip_windows_under_this (ZORDERINFO* zorder, PMAINWIN pWin, RECT* rcWin)
{
PZORDERNODE pNode;
PGCRINFO pGCRInfo;
pNode = zorder->pTopMost;
while (pNode->hWnd != (HWND)pWin)
pNode = pNode->pNext;
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 从剪切矩形私有堆中分配和释放剪切矩形
PCLIPRECT GUIAPI 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 rect failure!\n");
else
pRect->fromheap = FALSE;
}
}
#ifndef _LITE_VERSION
pthread_mutex_unlock (&pList->lock);
#endif
return pRect;
}
void GUIAPI 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.1 控件类和控件
如果读者曾经编写过 Windows 应用程序的话,就应该了解窗口类的概念。在 Windows 中,程序所建立的每个窗口,都对应着某种窗口类。这一概念和面向对象编程中的类、对象的关系类似。借用面向对象的术语,Windows 中的每个窗口实际都是某个窗口类的一个实例。在 X Window 编程中,也有类似的概念,比如我们建立的每一个 Widget,实际都是某个 Widget 类的实例。
这样,如果程序需要建立一个窗口,就首先要确保选择正确的窗口类,因为每个窗口类决定了对应窗口实例的表象和行为。这里的表象指窗口的外观,比如窗口边框宽度,是否有标题栏等等,行为指窗口对用户输入的响应。每一个 GUI 系统都会预定义一些窗口类,常见的有按钮、列表框、滚动条、编辑框等等。如果程序要建立的窗口很特殊,就需要首先注册一个窗口类,然后建立这个窗口类一个实例。这样就大大提高了代码的可重用性。
在 MiniGUI 中,我们认为主窗口通常是一种比较特殊的窗口。因为主窗口代码的可重用性一般很低,如果按照通常的方式为每个主窗口注册一个窗口类的话,则会导致额外不必要的存储空间,所以我们并没有在主窗口提供窗口类支持。但主窗口中的所有子窗口,即控件,均支持窗口类(控件类)的概念。MiniGUI 提供了常用的预定义控件类,包括按钮(包括单选钮、复选钮)、静态框、列表框、进度条、滑块、编辑框等等。程序也可以定制自己的控件类,注册后再创建对应的实例。清单 3 中的代码就创建了一个编辑框,一个按钮。
采用控件类和控件实例的结构,不仅可以提高代码的可重用性,而且还可以方便地对已有控件类进行扩展。比如,在需要建立一个只允许输入数字的编辑框时,就可以通过重载已有编辑框控件类而实现,而不需要重新编写一个新的控件类。在 MiniGUI 中,这种技术称为子类化或者窗口派生。子类化的方法有三种:
·一种是对已经建立的控件实例进行子类化,子类化的结果是只影响这一个控件实例;
·一种是对某个控件类进行子类化,将影响其后创建的所有该控件类的控件实例;
·最后一种是在某个控件类的基础上新注册一个子类化的控件类,不会影响原有控件类。在 Windows 中,这种技术又称为超类化。
在 MiniGUI 中,控件的子类化实际是通过替换已有的窗口过程实现的。清单 4 中的代码就通过控件类创建了两个子类化的编辑框,一个只能输入数字,而另一个只能输入字母:
清单 4 控件的子类化
#define IDC_CTRL1 100
#define IDC_CTRL2 110
#define IDC_CTRL3 120
#define IDC_CTRL4 130
#define MY_ES_DIGIT_ONLY 0x0001
#define MY_ES_ALPHA_ONLY 0x0002
static WNDPROC 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);
}
static int ControlTestWinProc (HWND hWnd, int message, WPARAM wParam, LPARAM lParam)
{
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 中,程序首先定义了一个窗口处理过程,即 RestrictedEditBox 函数。然后,在利用 CreateWindow 函数建立控件时,将其中两个编辑框的窗口处理过程通过 SetWindowCallbackProc 替换成了自己定义的 RestrictedEditBox 函数,并且将该函数返回的值(即老的控件窗口处理过程地址)保存在了 old_edit_box 变量中。在建立这些编辑框之后,它们的消息将首先由 RestrictedEditBox 函数处理,然后在某些情况下才由老的窗口处理过程处理。
限于篇幅,另外两种控件子类化的方法就不在这里讲述。
4.2 MiniGUI 中控件类的实现
MiniGUI 函数库实际维护了一个当前所有控件类的数据结构,其中包含了控件类名称以及对应的控件类信息。该数据结构实际是一个哈希表,哈希表的每个入口包含由一个指针,该指针指向所有名程以某个字母开头(不分大小写)的控件类信息链表。控件类信息结构定义如下:
#define MAXLEN_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);
// 控件处理过程
DWORD dwAddData; // 附加数据
int nUseCount; // 使用计数,即系统中属于该控件类的控件个数
struct _CTRLCLASSINFO* next;
// 下一个控件类信息结构
} CTRLCLASSINFO;
typedef CTRLCLASSINFO* PCTRLCLASSINFO;
在控件类的数据结构中包含了鼠标、光标、控件类的回调函数地址等等信息。在创建属于该控件类的控件时,这些信息会复制到控件数据结构中。这样,新的控件实例就继承了这种控件类的表象和行为。
该哈希表的哈希函数实际非常简单,它的返回值就是控件类名称首字母的英文字母表顺序值:
static int 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.3 MiniGUI 中控件的实现
控件结构相对复杂一些。其中包含了控件在父窗口中的位置信息、控件风格、扩展风格、控件鼠标、图标、控件回调函数地址等等:
typedef struct _CONTROL
{
/*
* 这些成员和 MAINWIN 结构一致.
*/
short DataType; // 内部使用的数据类型
short WinType; // 内部使用的窗口类型
int left, top; // 控件在父窗口中的位置
int right, bottom;
int cl, ct; // 控件客户区在父窗口中的位置
int cr, cb;
DWORD dwStyle; // 控件风格
DWORD dwExStyle; // 控件扩展风格
int iBkColor; // 背景颜色
HMENU hMenu; // 菜单句柄
HACCEL hAccel; // 加速键表句柄
HCURSOR hCursor; // 鼠标光标句柄
HICON hIcon; // 图标句柄
HMENU hSysMenu; // 系统菜单句柄
HDC privCDC; // 私有 DC 句柄
INVRGN InvRgn; // 控件的无效区域
PGCRINFO pGCRInfo; // 控件的全局剪切区域
PZORDERNODE pZOrderNode;
// Z 序节点
// 仅对具有 WS_EX_CTRLASMAINWIN 扩展风格的控件有效
PCARETINFO pCaretInfo; // 插入符消息
DWORD dwAddData; // 控件附加数据
DWORD dwAddData2; // 控件附加数据
int (*ControlProc) (HWND, int, WPARAM, LPARAM); // 控件消息处理过程
char* spCaption; // 控件标题
int id; // 控件标识符,整数
SCROLLBARINFO vscroll; // 垂直滚动条信息
SCROLLBARINFO hscroll; // 水平滚动条信息
PMAINWIN pMainWin; // 包含该控件的主窗口
struct _CONTROL* pParent;// 控件的父窗口
/*
* Child windows.
*/
struct _CONTROL* children;
// 控件的第一个子控件
struct _CONTROL* active;
// 当前活动子控件
struct _CONTROL* old_under_pointer;
// 老的鼠标鼠标所在子控件
/*
* 下面这些成员只对控件有效
*/
struct _CONTROL* next; // 下一个兄弟控件
struct _CONTROL* prev; // 前一个兄弟控件
PCTRLCLASSINFO pcci; // 指向控件所属控件类结构的指针
} CONTROL;
typedef CONTROL* PCONTROL;
很显然,只要将控件的回调函数地址进行替换,就可以非常方便地对控件进行子类化操作。值得一提的是,主窗口的结构定义和控件数据结构定义基本上是相同的,只是在某些成员上有一些小小的差别。
输入法提供了将标准键盘输入翻译为适当语种的文字的能力。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 消息,在切换到需要输入法的活动窗口时自动显示输入法窗口。
本文重点讲述了 MiniGUI 中的窗口剪切处理算法。这是任何一个多窗口系统首先要解决的问题。然后,本文介绍了 MiniGUI 中控件类和控件的实现。最后介绍了 MiniGUI 中输入法窗口的设计思路。
附:MiniGUI 的最新进展
2001 年元月 03 日,MiniGUI 的 0.9.98 版本发布。该版本包括一个我们专门针对 PDA 等嵌入式系统设计的 MiniGUI 版本,该版本称为 MiniGUI-Lite。下面是对 MiniGUI-Lite 简单介绍,将来我们还要撰文详细介绍 MiniGUI-Lite。
大家都知道,MiniGUI 采用了基于线程的体系结构,并且建立了基于线程的消息传递和窗口管理功能。但是,在许多系统中,这种基于线程的结构并不是很好。这是因为一些众所周知的原因造成的--Linux 线程,尽管可以提供最大程度上的数据共享,但却造成了系统体系结构的脆弱。如果某个线程因为非法的数据访问而终止运行,则整个进程都将受到影响。与线程结构相反的是采用传统的 UNIX IPC 机制建立窗口系统,即类似 X Window 的客户/服务器体系。这种体系结构有它的先天不足,主要是通常的 IPC 机制无法提供高效的数据复制,大量的 CPU 资源用于在各进程之间复制数据。在 PDA 等设备中,这种 CPU 资源的浪费将最终导致系统性能的降低以及设备耗电量的增加。
为了解决 MiniGUI 版本因为线程而引入的一些问题,同时也为了让 MiniGUI更加适合于嵌入式系统,我们决定开发一个 MiniGUI Lite 版本。这个版本的开发目的是:
1. 保持与原先 MiniGUI 版本在源代码级 98% 以上的兼容。 2. 不再使用 LinuxThreads。 3. 可以同时运行多个基于 MiniGUI Lite 的应用程序,即多个进程,并且提供前后台进程的切换。
显然,要同时满足上述三个目的,如果采用传统的 C/S 结构对现有 MiniGUI 进行改造,应该不难实现。但前面提到的传统 C/S 结构的缺陷却无法避免。经过对 PDA 等嵌入式系统的分析,我们发现,某些 PDA 产品具有运行多个任务的能力,但同一时刻在屏幕上进行绘制的程序,一般不会超过两个。因此,只要确保将这两个进程的绘制相互隔离,就不需要采用复杂的 C/S 结构处理多个进程窗口之间的互相剪切。也就是说,在这种产品中,如果采用基于传统 C/S 结构的多窗口系统,实际是一种浪费。
有了上述认识,我们对 MiniGUI-Lite 版本进行了如下简化设计:
1. 每个进程维护自己的主窗口 Z 序,同一进程创建的主窗口之间互相剪切。也就是说,除这个进程只有一个线程,只有一个消息循环之外,它与原有的 MiniGUI 版本之间没有任何区别。每个进程在进行屏幕绘制时,不需要考虑其他进程。 2. 建立一个简单的客户/服务器体系,但确保最小化进程间的数据复制功能。因此,在服务器和客户之间传递的数据仅限于输入设备的输入数据,以及客户和服务器之间的某些请求和响应数据。 3. 有一个服务器进程(mginit),它负责初始化一些输入设备,并且通过 UNIX Domain 套接字将输入设备的消息发送到前台的 MiniGUI Lite 客户进程。 4. 服务器和客户被分别限定在屏幕的某两个不相交矩形内进行绘制,同一时刻,只能有一个客户及服务器进行屏幕绘制。其他客户可继续运行,但屏幕输入被屏蔽。服务器可以利用 API 接口将某个客户切换到前台。同时,服务器和客户之间采用信号和 System V 信号量进行同步。 5. 服务器还采用 System V 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 采用了面向对象的技术实现了 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 和 Adobe Type1 字体等的支持。
在多字体和多字符集的抽象接口之上,MiniGUI 通过逻辑字体为应用程序提供了一致的接口。
本文重点介绍 MiniGUI 的逻辑字体、多字体和多字符集的实现,并以 EUCKR(韩文)字符集和 Adobe Type1 字体为例,说明如何在 MiniGUI 中实现一种新的字符集支持和新的字体类型支持。
在 MiniGUI 中,每个逻辑字体至少由一个单字节的设备字体组成。设备字体是直接与底层字体相关联的数据结构。每个设备字体有一个操作集(即 font_ops),其中包含了 get_char_width、get_char_bitmap 等抽象接口。每个 MiniGUI 所支持的字体类型,比如等宽光栅字体(RBF)、变宽光栅字体(VBF)、TrueType 字体、Adobe Type1 字体等均对应一组字体操作集。通过这个字体操作集,我们就可以从相应的字体文件中获得某个字符的点阵(对光栅字体而言)或者轮廓(对矢量字体而言)。之后,MiniGUI 上层的绘图函数就可以将这些点阵输出到屏幕上,最终就可以看到显示在屏幕上的文字。
在设备字体结构中,还有一个字符集操作集(即 charset_ops),其中包含了 len_first_char、char_offset、len_first_substr 等抽象接口。每个 MiniGUI 所支持的字符集,比如 ISO8859-x、GB2312、BIG5 等字符集均对应一组字符集操作集。通过这个字符集操作集,我们就可以对某个多种字符集混合的字符串进行文本分析。比如在“ABC中文”这个字符串中,头三个字符是属于 ISO8859 的字符,而“中文”是属于 GB2312 的字符。通过调用这两个字符集操作集中的函数,我们就可以了解该字符串中哪些字符是属于 ISO8859 的字符,哪些字符是属于 GB2312 的字符,甚至可以进行更加复杂的分析。比如,MiniGUI 中的 GetFirstWord 函数可以从这种字符串中获得第一个单词。比如“ABC DEF 中文”字符串中的第一个单词是“ABC”,而第二个单词是“DEF”,第三个单词和第四个单词分别是“中”和“文”。该函数的实现如下:
int GUIAPI GetFirstWord (PLOGFONT log_font, const char* mstr, int len,
WORDINFO* word_info)
{
DEVFONT* sbc_devfont = log_font->sbc_devfont;
DEVFONT* mbc_devfont = log_font->mbc_devfont;
if (mbc_devfont) {
int mbc_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 中,每个特定的字符集由对应的字符集操作集来表示。字符集操作集的定义如下(include/gdi.h。前面的数字表示在该文件中的行数,下同):
250 typedef 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, int mstrlen);
259 int (*char_offset) (const unsigned char* mchar);
260
261 int (*nr_chars_in_str) (const unsigned char* mstr, int mstrlen);
262
263 int (*is_this_charset) (const unsigned char* charset);
264
265 int (*len_first_substr) (const unsigned char* mstr, int mstrlen);
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, int mstrlen);
270
271 #ifndef _LITE_VERSION
272 unsigned short (*conv_to_uc16) (const unsigned char* mchar, int len);
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):
716 static 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 /************************* EUCKR Specific Operations ************************/
470 static int euckr_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 return 2;
484
485 return 0;
486 }
487
488 static 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
496 static 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 unsigned char* mstr, int mstrlen)
515 {
516 unsigned char ch1;
517 unsigned char ch2;
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 int euckr_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 static unsigned short euckr_conv_to_uc16 (const unsigned char* mchar, int len)
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 */
4.1 设备字体
在 MiniGUI 中,设备字体定义如下(include/gdi.h):
319 struct _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):
228 typedef struct _LOGFONT {
229 char type [LEN_FONT_NAME + 1];
230 char family [LEN_FONT_NAME + 1];
231 char charset [LEN_FONT_NAME + 1];
232 DWORD style;
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):
276 typedef 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, int expect);
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):
155 static 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 新设备字体的实现举例
这里以 Adobe Type1 字体的实现为例,说明如何在 MiniGUI 中实现一种新的设备字体。MiniGUI 借用了 T1Lib 函数库实现了对 Type1 字体的支持。
4.4.1 Type1 字体简介
Type1 矢量字体1格式由 Adobe 公司设计,并被该公司的ps标准支持。因此,它在Linux下也被支持得很好。它被 X和 ghostscript支持。一个典型的Type1字体包括一个afm(adobe font metric) 度量文件,一个外形文件,通常是一个pfb ( printer font binary) 或者 pfa (printer font ascii) 文件,外形文件包括所有的轮廓,而度量文件包含了所有的度量。比如紧排,连字等信息。
4.4.2 T1Lib 简介
T1Lib 是用 C 语言实现的一个库,它可以从 Adobe Type 1 字体生成位图。它可以使用X11R5 或者更新版本提供的光栅化工具的很多功能,但避免了其已知的缺点。当然,T1Lib完全可以在没有 X11 的环境下工作。T1Lib 可以被编译成静态或者动态库,从而可以方便地连接。
这里是T1Lib 的一些特性:
·字体通过运行时读取字库而被T1lib得知。即它是灵活可配置的。当然,它只支持Type 1字体。
字符或字符串只在需要时才被光栅化。
·对字符串光栅化时支持字符间紧排,并且可以利用一个AFM文件提供紧排信息,如果没有这个文件,T1Lib可以直接生成这些信息,也可以将其输出到一个文件以备后用。
·支持连字,连字是一个好的字体模型会提供的功能,目前,只有TEX和与其相关的软件包对连字支持得比较好。连字信息也包含在AFM文件里。
·支持旋转和各种仿射变换。支持字体扩展,倾斜。
·可以动态载入新的解码矢量。用新的解码矢量解析字体。
·支持5灰度的低分辨率和17灰度的高分辨率的反走样。
·字符串可以被添加下划线,上划线或者横线。
4.4.3 Adobe Type1 字体支持的实现
在 MiniGUI 设备字体定义中,有一个 data 字段可用来保存设备字体相关的数据结构。对 Type1 字体来讲,我们使用 TYPE1INFO和TYPE1INSTANCEINFO两个数据结构来存储这种设备字体的类信息和实例信息。
1) TYPE1INFO和TYPE1INSTANCEINFO 结构
这两个结构的定义如下(src/font/type1.h):
22 typedef struct tagTYPE1GLYPHINFO {
23 int font_id;
24 //BBox font_bbox;
25 //int ave_width;
26 BOOL valid;
27 } TYPE1INFO, *PTYPE1INFO;
28
29 typedef struct tagTYPE1INSTANCEINFO {
30 PTYPE1INFO type1_info;
31 int rotation;/*in tenthdegrees*/
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's info
44 * T1_SetChar, T1_SetString, T1_AASetSting, T1_AASetString all return a static
45 * glyph pointer, we save the related infomation 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 unsigned long last_bpp;
58 char * last_bits;
59
60 } TYPE1INSTANCEINFO, *PTYPE1INSTANCEINFO;
61
如前面所说,TYPE1INFO和TYPE1INSTANCEINFO数据结构来存储设备字符的类信息和实例信息。初始华时,其实只是注册一个模板,此时利用TYPE1INFO记住其在 T1lib中的Font ID,这里valid用来说明该设备字体是否初始化完毕。
当用户创建一逻辑字体时,如果用户选择的是Type1字体的某一种,就会调用 font_ops 的函数new_instance,该函数根据存在于 DevFont 的data的 TYPE1INFO 结构中的 id,以及用户提供的相关参数,构造一个TYPE1INSTANCEINFO类型的变量,并放入新的设备字体的私有数据data中。从而每个字体实例可以有自己的各种属性。如旋转度。
前面各个字段的意义可以根据名字推测出来,从csUnix2Pixel 开始则是为了实现的方便和高效而自己定义的一些变量,后面解释函数实现时将会说明。last*系列函数主要起缓冲的作用。
2) InitType1Fonts 和 TermType1Fonts 函数
这两个函数负责整个 Type 1 字体的初始化和终结。
InitType1Fonts 的主要任务是:初始化T1lib,根据配置文件提供的信息,将各种字体注册到T1lib,并为每一个字体生成一个 DevFont 结构,注册到系统中去。该结构中包括的 font_ops,是上层对Type 1字体各种操作的窗口。
其实主要的处理功能在 T1lib 中,每次程序向 T1lib 注册一个字体,T1lib会返回一个 Font ID,以后利用该ID 向T1lib请求关于对应字体的某些服务。
·TermType1Fonts 则是注销 Type1 字体,关闭T1lib。
·InitType1Fonts 注册向系统注册了用来处理 Abode Type1 字体的字体操作集,定义如下(src/font/type1.c):
780 static 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:某个字符从其原点到最左边像素点的水平距离,也可以称为该字符的left margin。
·rightSideBearing:某个字符从其原点到最右边像素点的水平距离,也可以称为该字符的right margin。
·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 则用来删除相关的数据结构。
面向对象技术在软件设计当中占有非常重要的地位,但面向对象并不是 C++ 等语言的专利。实际上,在诸如操作系统等系统软件当中,面向对象技术的使用是非常广泛的。利用 C 语言实现面向对象技术,不仅结构清晰,而且在执行效率等方面也有 C++ 等语言无法相比的优势。从本文描述的字体和字符集的实现当中我们可以看到,采用面向对象技术,将大大提高系统的灵活性和可扩展性。
MiniGUI 作为一个面向实时嵌入式系统的图形用户界面支持系统,对其执行效率、可定制、可扩展等方面有非常高的要求。为了提高系统的灵活性和可扩展性,我们在一些关键模块当中使用了面向对象的技术。实践表明,面向对象的技术在 MiniGUI 中的运用是成功的。
在 MiniGUI 0.3.xx 的开发中,我们引入了图形和输入抽象层(Graphics and Input Abstract Layer,GAL 和 IAL)的概念。抽象层的概念类似 Linux 内核虚拟文件系统的概念。它定义了一组不依赖于任何特殊硬件的抽象接口,所有顶层的图形操作和输入处理都建立在抽象接口之上。而用于实现这一抽象接口的底层代码称为“图形引擎”或“输入引擎”,类似操作系统中的驱动程序。这实际是一种面向对象的程序结构。利用 GAL 和 IAL,MiniGUI 可以在许多已有的图形函数库上运行,比如 SVGALib 和 LibGGI。并且可以非常方便地将 MiniGUI 移植到其他 POSIX 系统上,只需要根据我们的抽象层接口实现新的图形引擎即可。比如,在基于 Linux 的系统上,我们可以在 Linux FrameBuffer 驱动程序的基础上建立通用的 MiniGUI 图形引擎。实际上,包含在 MiniGUI 1.0.00 版本中的私有图形引擎(Native Engine)就是建立在 FrameBuffer 之上的图形引擎。一般而言,基于 Linux 的嵌入式系统均会提供 FrameBuffer 支持,这样私有图形引擎可以运行在一般的 PC 上,也可以运行在特定的嵌入式系统上。
相比图形来讲,将 MiniGUI 的底层输入与上层相隔显得更为重要。在基于 Linux 的嵌入式系统中,图形引擎可以通过 FrameBuffer 而获得,而输入设备的处理却没有统一的接口。在 PC 上,我们通常使用键盘和鼠标,而在嵌入式系统上,可能只有触摸屏和为数不多的几个键。在这种情况下,提供一个抽象的输入层,就显得格外重要。
本文将介绍 MiniGUI 的 GAL 和 IAL 接口,并介绍私有图形引擎和特定嵌入式系统下的输入引擎实现。
GAL 和 IAL 的结构是类似的,我们以 GAL 为例说明 MiniGUI GAL 和 IAL 抽象层的结构。
2.1 GAL 和图形引擎
参见图 1。系统维护一个已注册图形引擎数组,保存每个图形引擎数据结构的指针。系统利用一个指针保存当前使用的图形引擎。一般而言,系统中至少有两个图形引擎,一个是“哑”图形引擎,不进行任何实际的图形输出;一个是实际要使用的图形引擎,比如 LibGGI 或者 SVGALib,或者 Native Engine。每个图形引擎的数据结构定义了该图形引擎的一些信息,比如标识符、属性等,更重要的是,它实现了 GAL 所定义的各个接口,包括初始化和终止、图形上下文管理、画点处理函数、画线处理函数、矩形框填充函数、调色板函数等等。
如果在某个实际项目中所使用的图形硬件比较特殊,现有的图形引擎均不支持。这时,我们就可以安照 GAL 所定义的接口实现自己的图形引擎,并指定 MiniGUI 使用这种私有的图形引擎即可。这种软件技术实际就是面向对象多态性的具体体现。
利用 GAL 和 IAL,大大提高了 MiniGUI 的可移植性,并且使得程序的开发和调试变得更加容易。我们可以在 X Window 上开发和调试自己的 MiniGUI 程序,通过重新编译就可以让 MiniGUI 应用程序运行在特殊的嵌入式硬件平台上。
在代码实现上,MiniGUI 通过 GFX 数据结构来表示图形引擎,见清单 1。
清单 1 MiniGUI 中的图形引擎结构(src/include/gal.h)
55 typedef struct tagGFX
56 {
57 char* id;
58
59 // Initialization and termination
60 BOOL (*initgfx) (struct tagGFX* gfx);
61 void (*termgfx) (struct tagGFX* 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, int height, 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_pixel color);
94 int (*getfgcolor) (GAL_GC gc, gal_pixel* color);
95 int (*setfgcolor) (GAL_GC gc, gal_pixel color);
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_pixel pixel, gal_color* color);
100 int (*packcolors) (GAL_GC gc, void* buf, gal_color* colors, int len);
101 int (*unpackpixels) (GAL_GC gc, void* buf, gal_color* colors, int len);
102
103 // Palette operations
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, int w, 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, int w, 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, int bw,
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, int w, int h, int nx, int ny);
123 int (*crossblit) (GAL_GC src, int sx, int sy, int sw, int sh,
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_pixel pixel);
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 line operations
132 int (*drawvline) (GAL_GC gc, int x, int y, int h, gal_pixel pixel);
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 // Pixel operations
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, int x2, int y2,
144 gal_pixel pixel);
145 int (*rectangle) (GAL_GC gc, int l, int t, int r, int b,
146 gal_pixel pixel);
147
148 // Simple Character output
149 int (*putchar) (GAL_GC gc, int x, int y, char c);
150 int (*putstr) (GAL_GC gc, int x, int y, const char* str);
151 int (*getcharsize) (GAL_GC gc, int* width, int* height);
152 int (*setputcharmode) (GAL_GC gc, int bkmode);
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) (int exitcode);
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 #define BITSPERPHYPIXEL (cur_gfx->bits_per_phypixel)
170 #define WIDTHOFPHYGC (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 #define GAL_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 #define GAL_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 #define GAL_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)
255 void 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 // Move the 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.2 IAL 和输入引擎
如前所属,MiniGUI IAL 结构和 GAL 结构类似。在代码实现上,MiniGUI 通过 INPUT数据结构来表示输入引擎,见清单 3。
清单 3 MiniGUI 中的输入引擎结构(src/include/ial.h)
34 typedef struct tagINPUT
35 {
36 char* id;
37
38 // Initialization and termination
39 BOOL (*init_input) (struct tagINPUT *input, const char* mdev, const char* mtype);
40 void (*term_input) (void);
41
42 // Mouse operations
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 #define IAL_TermInput (*cur_input->term_input)
71 #define IAL_UpdateMouse (*cur_input->update_mouse)
72 #define IAL_GetMouseX (*cur_input->get_mouse_x)
73 #define IAL_GetMouseY (*cur_input->get_mouse_y)
74 #define IAL_SetMouseXY (*cur_input->set_mouse_xy)
75 #define IAL_GetMouseButton (*cur_input->get_mouse_button)
76 #define IAL_SetMouseRange (*cur_input->set_mouse_range)
77
78 #define IAL_UpdateKeyboard (*cur_input->update_keyboard)
79 #define IAL_GetKeyboardState (*cur_input->get_keyboard_state)
80 #define IAL_SuspendKeyboard (*cur_input->suspend_keyboard)
81 #define IAL_ResumeKeyboard (*cur_input->resume_keyboard)
82 #define IAL_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, PLWEVENT lwe)
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.m