MiniGUI |
目录
MiniGUI
体系结构
概览
引言
POSIX线程
基于PThread的微客户/服务器结构
多线程通讯的关键数据结构——消息队列
面向对象技术在MiniGUI中的应用
未来考虑
多窗口管理和控件及控件类
引言
窗口Z序
窗口剪切算法
主窗口和控件、控件类
输入法模块的设计
小结
逻辑字体以及多字体和多字符集实现
引言
逻辑字体、设备字体以及字符集之间的关系
MiniGUI中的字符集支持3.1字符集操作集
MiniGUI中的字体支持
小结
图形抽象层和输入抽象层及Native的实现
引言
MiniGUI的GAL和IAL定义
Native图形引擎的实现
Native输入引擎的实现
特定嵌入式系统上图形引擎和输入引擎实现
小结
开发指南
选择MiniGUI-Threads或者MiniGUI-Lite
消息循环和窗口过程
对话框和控件编程
使用GDI函数
MiniGUI1.1.0引入的新GDI功能和函数(1)
MiniGUI1.1.0引入的新GDI功能和函数(2)
MiniGUI提供的非GUI/GDI接口
MiniGUI和其他嵌入式Linux上的图形及图形用户界面系统
Lite的新改进
安装手册
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
#include
#include
#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中设备字体的名称格式如下: