Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1427186
  • 博文数量: 430
  • 博客积分: 9995
  • 博客等级: 中将
  • 技术积分: 4388
  • 用 户 组: 普通用户
  • 注册时间: 2006-05-24 18:04
文章存档

2013年(1)

2008年(2)

2007年(14)

2006年(413)

分类:

2006-06-13 16:31:58

 
MiniGUI

目 录



MiniGUI



体系结构

    为了帮助更多软件开发人员理解 MiniGUI及其编程,同时帮助更多的自由软件开发人员加入 MiniGUI 的开发,我们将撰写一系列文章介绍 MiniGUI 的体系结构。本文是系列文章的第一篇,将在整体上对 MiniGUI 的体系结构作一介绍。其中主要包括:线程的基本概念;基于 POSIX Thread 的微客户/服务器结构;用来同步微客户/服务器动作的关键数据结构――消息队列;面向对象技术在 MiniGUI 中的应用等等。最后,文章展望了我们计划在 MiniGUI 2.0 版开发中采用的体系结构。


概览

    到目前为止,MiniGUI 的最新发布版本是 0.9.96。我们将 0.9.xx 系列版本定位为 MiniGUI 1.0 版本的预览版。在 0.9.xx 版本足够稳定时,我们将发布 MiniGUI 1.0 版本,同时,目前的代码不会再进行重大调整。在 MiniGUI 1.0 版本发布之后,我们将立即着手开发 MiniGUI 2.0 版本。该版本预期将在体系结构上进行重大调整。


引言

1 引言
    为了吸引更多的自由软件程序员加入 MiniGUI 2.0 的开发,也为了更好地帮助 MiniGUI 程序员进行程序开发,我们将撰写一系列的文章介绍 MiniGUI 1.0 版本的体系结构,重点分析其中的一些缺点以及需要在 2.0 版本当中进行优化和改造的地方。介绍体系结构的文章计划如下:

    ·体系结构概览(本文)。将在整体上对 MiniGUI 1.0 的体系结构进行介绍。重点包括:线程的基本概念;多线程的微客户/服务器体系、多线程通讯的关键数据结构――消息队列;面向对象技术在 MiniGUI 中的应用等等。
    ·MiniGUI 的多窗口管理。将介绍 MiniGUI 的多窗口机制以及相关的窗口类技术。其中涉及到窗口剪切处理和 Z 序,消息传递,控件类设计和输入法模块设计等等。
    ·MiniGUI 的图形设备管理。重点介绍 MiniGUI 是如何处理窗口绘制的。其中主要包括图形上下文的概念,坐标映射,图形上下文的局部、全局和有效剪切域的概念等等。
    ·图形抽象层和输入抽象层。图形抽象层(GAL)和输入抽象层(IAL)大大提高了 MiniGUI 的可移植性,并将底层图形设备和上层接口分离开来。这里将重点介绍 MiniGUI 的 GAL 和 IAL 接口,并以 EP7211 等嵌入式系统为例,说明如何将 MiniGUI 移植到新的嵌入式平台上。
    ·多字体和多字符集支持。MiniGUI 采用逻辑字体实现多字体和多字符集处理。这一技术成功应用了面向对象技术,通过单一的逻辑接口,可以实现对各种字符集以及各种字体的支持。



POSIX 线程

2 POSIX 线程

    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
#include
#include

#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 系统来说,虽然缺少了地址保护,但运行效率却是最高的。



基于 PThread 的微客户/服务器结构

3 基于 PThread 的微客户/服务器结构

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 多线程通讯的关键数据结构--消息队列

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 检查这些标志时发现有某个定时器到期才会获得一个定时器消息。也就是说,定时器消息是不排队的。这样就解决了排队时可能塞满消息队列的问题。



面向对象技术在 MiniGUI 中的应用

5 面向对象技术在 MiniGUI 中的应用

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 时,首先要对文本进行分析,以便判断是否是一个属于该字符集的双字节字符。



未来考虑

6 在 MiniGUI 2.0 中的考虑

    尽管 MiniGUI 采用多线程机制实现了一个小巧、高效的窗口系统,但有很多理由希望 MiniGUI 能够采用多进程机制实现(尽管多进程机制可能带来通讯上的额外开支):

    ·良好的地址保护。窗口本身的崩溃不会影响 MiniGUI 的运行,而目前的多线程机制无法提供地址保护。
    ·信号处理上的问题。在多线程程序中,所有的多线程共享同一个信号处理方式,包括是否忽略、是否捕获等等。这对某些大型软件是很难接受的。
    ·多线程程序对程序员要求较高。在编写多线程程序时,通常要考虑到函数的“线程安全”问题,即函数是否是可重入的,因此,我们通常不能使用全局或者静态变量。

    鉴于上述需求,我们将在接下来的 MiniGUI 2.0 开发中,进行一些体系结构上的调整,其中最为重要的就是采用进程机制替代线程机制。




多窗口管理和控件及控件类

    本文是 MiniGUI 体系结构系列文章的第二篇,重点介绍 MiniGUI 的多窗口机制以及相关的窗口类技术。其中涉及到窗口 Z 序、窗口剪切、控件类和控件以及输入法模块设计等等。




引言

1 引言

    在任何一个足够复杂的 GUI 系统中,处理窗口之间的互相剪切是其首要解决的问题。因为多窗口系统首先要确保一个窗口中的绘制输出不会影响到另外一个窗口。为此,GUI 系统一般要利用 Z 序来管理窗口之间的互相剪切关系。根据窗口在 Z 序中所处的位置,GUI 系统要计算每个窗口受剪切的区域,即剪切域。通常,窗口的剪切域定义为互不相交的矩形集合。GUI 系统的底层图形引擎在进行输出时,要根据当前输出的剪切域进行输出的剪切操作。从而保证窗口的绘制输出不会互相影响。因为任何一个窗口的创建、销毁、隐 藏、显示均有可能影响其他窗口的剪切域,所以首先要有一个高效的剪切域维护算法。本文将详细描述 MiniGUI 中的剪切域生成算法。

    许多人对控件(或者部件)的概念已经相当熟悉了。控件可以理解为主窗口中的子窗口。这些子窗口的行为和主窗口一样,即能够接收键盘和鼠标等外部输入,也可 以在自己的区域内进行输出――只是它们的所有活动被限制在主窗口中。MiniGUI 也支持子窗口,并且可以在子窗口中嵌套建立子窗口。我们将 MiniGUI 中的所有子窗口均称为控件。

    在 Windows 或 X Window 中,系统会预先定义一些控件类,当利用某个控件类创建控件之后,所有属于这个控件类的控件均会具有相同的行为和显示。利用这些技术,可以确保一致的人机操 作界面,而对程序员来讲,可以像搭积木一样地组建图形用户界面。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 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 主窗口和控件、控件类

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;

    很显然,只要将控件的回调函数地址进行替换,就可以非常方便地对控件进行子类化操作。值得一提的是,主窗口的结构定义和控件数据结构定义基本上是相同的,只是在某些成员上有一些小小的差别。




输入法模块的设计

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 线程,尽管可以提供最大程度上的数据共享,但却造成了系统体系结构的脆弱。如果某个线程因为非法的数据访问而终止运行,则整个进程都将受到影响。与线程结 构相反的是采用传统的 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 的逻辑字体支持,主要内容涉及 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 和 Adobe Type1 字体等的支持。

    在多字体和多字符集的抽象接口之上,MiniGUI 通过逻辑字体为应用程序提供了一致的接口。

    本文重点介绍 MiniGUI 的逻辑字体、多字体和多字符集的实现,并以 EUCKR(韩文)字符集和 Adobe Type1 字体为例,说明如何在 MiniGUI 中实现一种新的字符集支持和新的字体类型支持。




逻辑字体、设备字体以及字符集之间的关系

2 逻辑字体、设备字体以及字符集之间的关系

    在 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中的字符集支持3.1字符集操作集

3 MiniGUI 中的字符集支持 3.1 字符集操作集

    在 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 */




MiniGUI中的字体支持

4 MiniGUI 中的字体支持

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 中设备字体的名称格式如下:

--