MiniGUI

目 录

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


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

[目录]


基于 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 &lt; NR_CHARSETS; i++) {
741         if ((*Charsets [i]-&gt;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 中设备字体的名称格式如下:

<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 则用来删除相关的数据结构。


[目录]


小结

5 小结

    面向对象技术在软件设计当中占有非常重要的地位,但面向对象并不是 C++ 等语言的专利。实际上,在诸如操作系统等系统软件当中,面向对象技术的使用是非常广泛的。利用 C 语言实现面向对象技术,不仅结构清晰,而且在执行效率等方面也有 C++ 等语言无法相比的优势。从本文描述的字体和字符集的实现当中我们可以看到,采用面向对象技术,将大大提高系统的灵活性和可扩展性。

    MiniGUI 作为一个面向实时嵌入式系统的图形用户界面支持系统,对其执行效率、可定制、可扩展等方面有非常高的要求。为了提高系统的灵活性和可扩展性,我们在一些关键模块当中使用了面向对象的技术。实践表明,面向对象的技术在 MiniGUI 中的运用是成功的。


[目录]


图形抽象层和输入抽象层及Native的实现

    本文是 MiniGUI 体系结构系列文章的第四篇。图形抽象层(GAL)和输入抽象层(IAL)大大提高了 MiniGUI 的可移植性,并将底层图形设备和上层接口分离开来。这里将重点介绍 MiniGUI 的 GAL 和 IAL 接口,并以最新的 MiniGUI-Lite 版本为例,介绍基于 Linux FrameBuffer 的 Native 图形引擎的实现,以及特定嵌入式系统上输入引擎的实现。
[目录]


引言

1 引言

在 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 接口,并介绍私有图形引擎和特定嵌入式系统下的输入引擎实现。


[目录]


MiniGUI的GAL和IAL定义

2 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.me.event == ME_LEFTDOWN ||
199                         old_lwe.data.me.event == ME_RIGHTDOWN ||
200                         old_lwe.data.me.event == ME_MIDDLEDOWN))) {
201                 // reset delay time
202                 timeout_count = timer_counter + timeout_threshold;
203             }
204
205             // reset delay time
206             lwe->type = LWETYPE_TIMEOUT;
207             lwe->count = timer_counter;
208             return 1;
209         }
210         return 0;
211     }
212
213     timeout_count = timer_counter + timeout_threshold;
214     // There was a event occurred.
215     if (event & IAL_MOUSEEVENT) {
216         lwe->type = LWETYPE_MOUSE;
217         if (RefreshCursor(&me->x, &me->y, &button)) {
218             me->event = ME_MOVED;
219             time1 = 0;
220             time2 = 0;
221
222             if (oldbutton == button)
223                 return 1;
224         }
225
226         if ( !(oldbutton & IAL_MOUSE_LEFTBUTTON) &&
227               (button & IAL_MOUSE_LEFTBUTTON) )
228         {
229             if (time1) {
230                 interval = timer_counter - time1;
231                 if (interval <= dblclicktime)
232                     me->event = ME_LEFTDBLCLICK;
233                 else
234                     me->event = ME_LEFTDOWN;
235                 time1 = 0;
236             }
237             else {
238                 time1 = timer_counter;
239                 me->event = ME_LEFTDOWN;
240             }
241             goto mouseret;
242         }
243
244         if ( (oldbutton & IAL_MOUSE_LEFTBUTTON) &&
245              !(button & IAL_MOUSE_LEFTBUTTON) )
246         {
247             me->event = ME_LEFTUP;
248             goto mouseret;
249         }
250
251         if ( !(oldbutton & IAL_MOUSE_RIGHTBUTTON) &&
252               (button & IAL_MOUSE_RIGHTBUTTON) )
253         {
254             if (time2) {
255                 interval = timer_counter - time2;
256                 if (interval <= dblclicktime)
257                     me->event = ME_RIGHTDBLCLICK;
258                 else
259                     me->event = ME_RIGHTDOWN;
260                 time2 = 0;
261             }
262             else {
263                 time2 = timer_counter;
264                 me->event = ME_RIGHTDOWN;
265             }
266             goto mouseret;
267         }
268
269         if ( (oldbutton & IAL_MOUSE_RIGHTBUTTON) &&
270             !(button & IAL_MOUSE_RIGHTBUTTON) )
271         {
272             me->event = ME_RIGHTUP;
273             goto mouseret;
274         }
275     }
276
277     if(event & IAL_KEYEVENT) {
278         lwe->type = LWETYPE_KEY;
279         keystate = IAL_GetKeyboardState ();
280         for(i = 0; i < NR_KEYS; i++) {
281             if(!oldkeystate[i] && keystate[i]) {
282                  ke->event = KE_KEYDOWN;
283                  ke->scancode = i;
284                  olddownkey = i;
285                  break;
286             }
287             if(oldkeystate[i] && !keystate[i]) {
288                  ke->event = KE_KEYUP;
289                  ke->scancode = i;
290                  break;
291             }
292         }
293         if (i == NR_KEYS) {
294             ke->event = KE_KEYDOWN;
295             ke->scancode = olddownkey;
296         }
297
298         make = (ke->event == KE_KEYDOWN)?1:0;
299
300         if (i != NR_KEYS) {
301             unsigned leds;
302
303             switch (ke->scancode) {
304                 case SCANCODE_CAPSLOCK:
305                     if (make && caps_off) {
306                         capslock = 1 - capslock;
307                         leds = slock | (numlock << 1) | (capslock << 2);
308                         IAL_SetLeds (leds);
309                         status = (DWORD)leds << 16;
310                     }
311                     caps_off = 1 - make;
312                 break;
313
314                 case SCANCODE_NUMLOCK:
315                     if (make && num_off) {
316                         numlock = 1 - numlock;
317                         leds = slock | (numlock << 1) | (capslock << 2);
318                         IAL_SetLeds (leds);
319                         status = (DWORD)leds << 16;
320                     }
321                     num_off = 1 - make;
322                 break;
323
324                 case SCANCODE_SCROLLLOCK:
325                     if (make & slock_off) {
326                         slock = 1 - slock;
327                         leds = slock | (numlock << 1) | (capslock << 2);
328                         IAL_SetLeds (leds);
329                         status = (DWORD)leds << 16;
330                     }
331                     slock_off = 1 - make;
332                     break;
333
334                 case SCANCODE_LEFTCONTROL:
335                     control1 = make;
336                     break;
337
338                 case SCANCODE_RIGHTCONTROL:
339                     control2 = make;
340                     break;
341
342                 case SCANCODE_LEFTSHIFT:
343                     shift1 = make;
344                     break;
345
346                 case SCANCODE_RIGHTSHIFT:
347                     shift2 = make;
348                     break;
349
350                 case SCANCODE_LEFTALT:
351                     alt1 = make;
352                     break;
353
354                 case SCANCODE_RIGHTALT:
355                     alt2 = make;
356                     break;
357
358             }
359
360             status &= 0xFFFFF0C0;
361
362             status |= (DWORD)((capslock << 8) |
363                              (numlock << 7)   |
364                              (slock << 6)     |
365                              (control1 << 5)  |
366                              (control2 << 4)  |
367                              (alt1 << 3)      |
368                              (alt2 << 2)      |
369                              (shift1 << 1)    |
370                              (shift2));
371
372             // Mouse button status
373             if (oldbutton & IAL_MOUSE_LEFTBUTTON)
374                 status |= 0x00000100;
375             else if (oldbutton & IAL_MOUSE_RIGHTBUTTON)
376                 status |= 0x00000200;
377         }
378         ke->status = status;
379         SHAREDRES_SHIFTSTATUS = status;
380         memcpy (oldkeystate, keystate, NR_KEYS);
381         memcpy (&old_lwe, lwe, sizeof (LWEVENT));
382         return 1;
383     }
384
385     old_lwe.type = 0;
386     return 0;
387
388 mouseret:
389     status &= 0xFFFFF0FF;
390     oldbutton = button;
391     if (oldbutton & IAL_MOUSE_LEFTBUTTON)
392         status |= 0x00000100;
393     if (oldbutton & IAL_MOUSE_RIGHTBUTTON)
394         status |= 0x00000200;
395     me->status = status;
396     SHAREDRES_SHIFTSTATUS = status;
397     memcpy (&old_lwe, lwe, sizeof (LWEVENT));
398     return 1;
399 }
#endif

从这段代码中可以看出,对定点设备来讲,比如鼠标或者触摸屏,MiniGUI 能够自动识别移动信息,也能够自动识别用户的单击和双击事件。这样,底层引擎只需提供位置信息和当前的按键状态信息就可以了。对类似键盘的东西,MiniGUI 也能够自动进行重复处理。当一个按键按下一段时间之后,MiniGUI 将连续发送该按键的消息给上层处理。对特定的嵌入式系统来讲,可以将某些按键映射为 PC 的某些键盘键,上层只需处理这些键盘键消息的按下和释放即可。这样,嵌入式系统上的某些键的功能就可以在 PC 上进行模拟了。

[目录]


Native图形引擎的实现

3 Native 图形引擎的实现

Native 图形引擎的图形驱动程序已经提供了基于Linux内核提供FrameBuffer之上的驱动,目前包括对线性 2 bpp、4bpp、8bpp和 16bpp 显示模式的支持。前面已经看到,GAL提供的接口函数大多数与图形相关,它们主要就是通过调用图形驱动程序来完成任务的。图形驱动程序屏蔽了底层驱动的细节,完成底层驱动相关的功能,而不是那么硬件相关的一些功能,如一些画圆,画线的GDI 函数。

下面基于已经实现的基于FrameBuffer 的驱动程序,讲一些实现上的细节。首先列出的核心数据结构 SCREENDEVICE。这里主要是为了讲解方便,所以删除了一些次要的变量或者函数。

                              清单 5  Native 图形引擎的核心数据结构

typedef struct _screendevice {
    int xres;       /* X screen res (real) */
    int yres;       /* Y screen res (real) */
    int planes;     /* # planes*/
    int bpp;        /* # bits per pixel*/
    int linelen;    /* line length in bytes for bpp 1,2,4,8, line length in pixels for bpp 16, 24, 32*/
    int size;       /* size of memory allocated*/
    gfx_pixel gr_foreground;      /* current foreground color */
    gfx_pixel gr_background;      /* current background color */
    int     gr_mode;
    int flags;      /* device flags*/
    void *  addr;       /* address of memory allocated (memdc or fb)*/

    PSD (*Open)(PSD psd);
    void    (*Close)(PSD psd);
    void    (*SetPalette)(PSD psd,int first,int count,gfx_color *cmap);
    void    (*GetPalette)(PSD psd,int first,int count,gfx_color *cmap);
    PSD (*AllocateMemGC)(PSD psd);
    BOOL    (*MapMemGC)(PSD mempsd,int w,int h,int planes,int bpp, int linelen,int size,void *addr);
    void    (*FreeMemGC)(PSD mempsd);
    void    (*FillRect)(PSD psd,int x,int y,int w,int h,gfx_pixel c);
    void     (*DrawPixel)(PSD psd, int x, int y, gfx_pixel c);
    gfx_pixel (*ReadPixel)(PSD psd, int x, int y);
    void    (*DrawHLine)(PSD psd, int x, int y, int w, gfx_pixel c);
    void    (*PutHLine) (GAL gal, int x, int y, int w, void* buf);
    void    (*GetHLine) (GAL gal, int x, int y, int w, void* buf);
    void    (*DrawVLine)(PSD psd, int x, int y, int w, gfx_pixel c);
    void    (*PutVLine) (GAL gal, int x, int y, int w, void* buf);
    void    (*GetVLine) (GAL gal, int x, int y, int w, void* buf);
    void (*Blit)(PSD dstpsd, int dstx, int dsty, int w, int h, PSD srcpsd, int srcx, int srcy);
    void    (*PutBox)( GAL gal, int x, int y, int w, int h, void* buf );
    void    (*GetBox)( GAL gal, int x, int y, int w, int h, void* buf );
    void    (*PutBoxMask)( GAL gal, int x, int y, int w, int h, void *buf);
    void    (*CopyBox)(PSD psd,int x1, int y1, int w, int h, int x2, int y2);
} SCREENDEVICE;

上面PSD 是 SCREENDEVICE 的指针,GAL 是GAL 接口的数据结构。

我们知道,图形显示有个显示模式的概念,一个像素可以用一位比特表示,也可以用2,4,8,15,16,24,32个比特表示,另外,VGA16标准模式使用平面图形模式,而VESA2.0使用的是线性图形模式。所以即使是同样基于Framebuffer 的驱动,不同的模式也要使用不同的驱动函数:画一个1比特的单色点和画一个24位的真彩点显然是不一样的。

所以图形驱动程序使用了子驱动程序的概念来支持各种不同的显示模式,事实上,它们才是最终的功能函数。为了保持数据结构在层次上不至于很复杂,我们通过图形驱动程序的初始函数Open直接将子驱动程序的各功能函数赋到图形驱动程序的接口函数指针,从而初始化结束就使用一个简单的图形驱动接口。下面是子图形驱动程序接口(清单 6)。

                     清单 6  Native 图形引擎的子驱动程序接口

typedef struct {
    int  (*Init)(PSD psd);
    void     (*DrawPixel)(PSD psd, int x, int y, gfx_pixel c);
    gfx_pixel (*ReadPixel)(PSD psd, int x, int y);
    void    (*DrawHLine)(PSD psd, int x, int y, int w, gfx_pixel c);
    void    (*PutHLine) (GAL gal, int x, int y, int w, void* buf);
    void    (*GetHLine) (GAL gal, int x, int y, int w, void* buf);
    void    (*DrawVLine)(PSD psd, int x, int y, int w, gfx_pixel c);
    void    (*PutVLine) (GAL gal, int x, int y, int w, void* buf);
    void    (*GetVLine) (GAL gal, int x, int y, int w, void* buf);
    void (*Blit)(PSD dstpsd, int dstx, int dsty, int w, int h, PSD srcpsd, int srcx, int srcy);
    void    (*PutBox)( GAL gal, int x, int y, int w, int h, void* buf );
    void    (*GetBox)( GAL gal, int x, int y, int w, int h, void* buf );
    void    (*PutBoxMask)( GAL gal, int x, int y, int w, int h, void *buf);
    void    (*CopyBox)(PSD psd,int x1, int y1, int w, int h, int x2, int y2);
} SUBDRIVER, *PSUBDRIVER;

可以看到,该接口中除了 Init 函数指针外,其他的函数指针都与图形驱动程序接口中的函数指针一样。这里的Init 函数主要用来完成图形驱动部分与显示模式相关的初始化任务。

下面介绍SCREENDEVICE数据结构,这样基本上就可以清楚图形引擎了。

一个SCREENDEVICE代表一个屏幕设备,它即可以对应物理屏幕设备,也可以对应一个内存屏幕设备,内存屏幕设备的存在主要是为了提高GDI 质量,比如我们先在内存生成一幅位图,再画到屏幕上,这样给用户的视觉效果就比较好。

首先介绍几个变量。

xres 表示屏幕的宽 (以像素为单位);
yres 表示屏幕的高 (以像素为单位);
planes :当处于平面显示模式时,planes 用于记录所使用的平面数,如平面模式相对的时线性模式,此时该变量没有意义。通常将其置为0。
bpp :表示每个像素所使用的比特数,可以为1,2,4,8,15,16,24,32。
linelen :对与1,2,4,8比特每像素模式,它表示一行像素使用的字节数,对于大于8比特每像素模式,它表示一行的像素总数。
size :表示该显示模式下该设备使用的内存数。linelen 和 size 的存在主要是为了方便为内存屏幕设备分配内存。
gr_foreground 和 gr_background :表示该内存屏幕的前景颜色和背景颜色,主要被一些GDI 函数使用。
gr_mode :说明如何将像素画到屏幕上,可选值为:MODE_SET MODE_XOR MODE_OR MODE_AND MODE_MAX,比较常用的是MODE_SET和MODE_XOR
flags :该屏幕设备的一些选项,比较重要的是 PSF_MEMORY 标志,表示该屏幕设备代表物理屏幕设备还是一个内存屏幕设备。
addr :每个屏幕设备都有一块内存空间用来作为存储像素。addr 变量记录了这个空间的起始地址。
下面介绍各接口函数:

Open,Close
基本的初始化和终结函数。前面已经提到,在 Open 函数里要选择子图形驱动程序,将其实现的函数赋给本 PSD 结构的函数指针。这里我讲讲基于Frambebuffer 的图形引擎的初始化。

fb_open 首先打开Framebuffer的设备文件 /dev/fb0,然后利用 ioctl 读出当前Framebuffer的各种信息。填充到PSD 结构中。并且根据这些信息选出子驱动程序。程序当前支持fbvga16,fblin16,fblin8,即VGA16 标准模式,VESA线性16位模式,VESA线性8位模式。然后将当前终端模式置于图形模式。并保存当前的一些系统信息如调色板信息。最后,系统利用mmap 将 /dev/fb0 映射到内存地址。以后程序访问 /dev/fb0 就像访问一个数组一样简单。当然,这是对线性模式而言的,如果是平面模式,问题要复杂的多。光从代码来看,平面模式的代码是线性模式的实现的将近一倍。后面的难点分析里将讲解这个问题。

SetPalette,GetPalette
当使用8位或以下的图形模式时,要使用系统调色板。这里是调色板处理函数,它们和Windows API 中的概念类似,linux 系统利用 ioctl 提供了处理调色板的接口。

AllocateMemGC,MapMemGC,FreeMemGC
前面屡次提到内存屏幕的概念,内存屏幕是一个伪屏幕,在对屏幕图形操作过程中,比如移动窗口,我们先生成一个内存屏幕,将物理屏幕的一个区域拷贝到内存屏幕,再拷贝到物理屏幕的新位置,这样就减少了屏幕直接拷贝的延时。AllocateMemGC 用于给内存屏幕分配空间,MapMemGC 做一些初始化工作,而FreeMemGC 则释放内存屏幕。

DrawPixel,ReadPixel,DrawHLine,DrawVLine,FillRect
这些是底层图形函数。分别是画点,读点,画水平线,画竖直线,画一个实心矩形。之所以在底层实现这么多函数,是为了提高效率。图形函数支持多种画图模式,常用的有直接设置,亦或,Alpha混合模式,从而可以支持各种图形效果。

PutHLine,GetHLine,PutVLine,GetVLine,PutBox,GetBox,PutBoxMask
Get* 函数用于从屏幕拷贝像素到一块内存区,而Put*函数用于将存放于内存区的像素画到屏幕上。PutBoxMask 与PutBox的唯一区别是要画的像素如果是白色,就不会被画到屏幕上,从而达到一种透明的效果。

从上面可以看到,这些函数的第一个参数是GAL类型而不是PSD类型,这是因为它们需要GAL层的信息以便在函数内部实现剪切功能。之所以不和其他函数一样在上层实现剪切,是因为这里的剪切比较特殊。比如PutBox,

在剪切输出域时,要同时剪切在缓冲中待输出的像素:超出剪切域的像素不应该被输出。所以,剪切已经不单纯是对线,矩形等GDI对象的剪切。对像素的剪切当然需要知道像素的格式,这些只是为底层所有,所以为了实现高效的剪切,我们选择在底层实现它们。这里所有的函数都有两个部分:先是剪切,再是读或者写像素。

Blit,CopyBox
Blit 用于在不同的屏幕设备(物理的或者内存的)之间拷贝一块像素点,CopyBox则用于在同一屏幕上实现区域像素的拷贝。如果使用的是线性模式,Blit的实现非常简单,直接memcpy 就可以了,而CopyBox 为了防止覆盖问题,必须根据不同的情况,采用不同的拷贝方式,比如从底到顶底拷贝,当新老位置在同一水平位置并且重复时,则需要利用缓冲间接拷贝。如果使用平面显示模式,这里就比较复杂了。因为内存设备总是采用线性模式的,所以就要判断是物理设备还是内存设备,再分别处理。这也大大地增加了fbvga16实现的代码。

[目录]


Native输入引擎的实现

4 Native 输入引擎的实现

4.1 鼠标驱动程序
鼠标驱动程序非常简单,抽象意义上讲,初始化鼠标后,每次用户移动鼠标,就可以得到一个X 和 Y 方向上的位移值,驱动程序内部维护鼠标的当前位置,用户移动了鼠标后,当前位置被加上位移值,并通过上层Cursor支持,反映到屏幕上,用户就会认为鼠标被他正确地“移动”了。

事实上,鼠标驱动程序的实现是利用内核或者其他驱动程序提供的接口来完成任务的。Linux 内核驱动程序使用设备文件对大多数硬件进行了抽象,比如,我们眼中的 ps/2 鼠标就是 /dev/psaux, 鼠标驱动程序接口如清单 7 所示。

                       清单 7  Native 输入引擎的鼠标驱动程序接口

typedef struct _mousedevice {
    int (*Open)(void);
    void    (*Close)(void);
    int (*GetButtonInfo)(void);
    void    (*GetDefaultAccel)(int *pscale,int *pthresh);
    int (*Read)(int *dx,int *dy,int *dz,int *bp);
    void (*Suspend)(void);
    void (*Resume)(void);
} MOUSEDEVICE;

现在有各种各样的鼠标,例如ms 鼠标, ps/2 鼠标,总线鼠标,gpm 鼠标,它们的主要差别在于初始化和数据包格式上。

例如,打开一个GPM 鼠标非常简单,只要将设备文件打开就可以了,当前终端被切换到图形模式时,GPM 服务程序就会把鼠标所有的位移信息放到设备文件中去。

static int GPM_Open(void)
{
    mouse_fd = open(GPM_DEV_FILE, O_NONBLOCK);
    if (mouse_fd < 0)
        return -1;
    return mouse_fd;
}

对于PS/2 鼠标,不但要打开它的设备文件,还要往该设备文件写入控制字符以使得鼠标能够开始工作。

static int PS2_Open(void)
{
uint8 initdata_ps2[] = { PS2_DEFAULT, PS2_SCALE11, PS2_ENABLE };
    mouse_fd = open(PS2_DEV_FILE, O_RDWR | O_NOCTTY | O_NONBLOCK);
    if (mouse_fd < 0)
            return -1;
    write(mouse_fd, initdata_ps2, sizeof(initdata_ps2));
    return mouse_fd;
}

各鼠标的数据包格式是不一样的。而且在读这些数据时,首先要根据内核驱动程序提供的格式读数据,还要注意同步:每次扫描到一个头,才能读后面相应的数据,象Microwindows由于没有同步,在某些情况下,鼠标就会不听“指挥”。

鼠标驱动程序中,还有一个“加速”的概念。程序内部用两个变量:scale 和thresh 来表示。当鼠标的位移超过 thresh 时,就会被放大 scale 倍。这样,最后的位移就是:

        dx = thresh + (dx - thresh) * scale;
        dy = thresh + (dy - thresh) * scale;

至此,mouse driver 基本上很清楚了,上面的接口函数中GetButtonInfo用来告诉调用者该鼠标支持那些button, suspend 和resume 函数是用来支持虚屏切换的,下面的键盘驱动程序也一样。

4.2 键盘驱动程序
在实现键盘驱动程序中遇到的第一个问题就是使用设备文件 /dev/tty还是 /dev/tty0。

# echo 1 > /dev/tty0
# echo 1 > /dev/tty

结果都将把1 输入到当前终端上。另外,如果从伪终端上运行它们,则第一条指令会将 1 输出到控制台的当前终端,而第二条指令会把 1 输出到当前伪终端上。从而 tty0 表示当前控制台终端,tty 表示当前终端(实际是当前进程控制终端的别名而已)。

tty0 的设备号是 4,0

tty1 的设备号是 5, 0

/dev/tty 是和进程的每一个终端联系起来的,/dev/tty 的驱动程序所做的只是把所有的请求送到合适的终端。

缺省情况下,/dev/tty 是普通用户可读写的,而/dev/tty0 则只有超级用户能够读写,主要是基于这个原因,我们目前使用 /dev/tty 作为设备文件。后面所有有关终端处理的程序的都采用它作为当前终端文件,这样也可以和传统的 Unix 相兼容。

键盘驱动程序接口如清单 8 所示。

                     清单 8  Native 输入引擎的键盘驱动程序接口

typedef struct _kbddevice {
    int  (*Open)(void);
    void (*Close)(void);
    void (*GetModifierInfo)(int *modifiers);
    int  (*Read)(unsigned char *buf,int *modifiers);
    void (*Suspend)(void);
    void (*Resume)(void);
} KBDDEVICE;

基本原理非常简单,初始化时打开 /dev/tty,以后就从该文件读出所有的数据。由于MiniGUI 需要捕获 KEY_DOWN 和 KEY_UP 消息,键盘被置于原始(raw)模式。这样,程序从 /dev/tty 中直接读出键盘的扫描码,比如用户按下A 键,就可以读到158,放下,又读到30。原始模式下,程序必须自己记下各键的状态,特别是shift,ctrl,alt,caps lock 等,所以程序维护一个数组,记录了所有键盘的状态。

这里说明一下鼠标移动,按键等事件是如何被传送到上层消息队列的。MiniGUI工作在用户态,所以它不可能利用中断这种高效的机制。没有内核驱动程序的支持,它也很难利用信号等Unix系统的IPC机制。MiniGUI可以做到的就是看 /dev/tty, /dev/mouse 等文件是否有数据可以读。上层通过不断调用 GAL_WaitEvent 尝试读取这些文件。这也是线程Parser的主要任务。GAL_WaitEvent 主要利用了系统调用select 这一类Unix系统中地位仅次于ioctl的系统调用完成该功能。并将等待到的事件作为返回值返回。

至此介绍了键盘和鼠标的驱动程序,作为简单的输入设备,它们的驱动是非常简单的。事实上,它们的实现代码也比较少,就是在嵌入式系统中要使用的触摸屏,如果操作系统内核支持,其驱动程序也是非常简单的:它只不过是一种特殊的鼠标。

[目录]


特定嵌入式系统上图形引擎和输入引擎实现

5 特定嵌入式系统上图形引擎和输入引擎实现

如前所述,基于 Linux 的嵌入式系统,其内核一般具备对 FrameBuffer 的支持,从而可以利用已有的 Native 图形引擎。在 MiniGUI 代码中,可通过调整 src/gal/native/native.h 中的宏定义而定义函数库中是否包含特定的图形引擎驱动程序(清单 9):

                          清单 9  定义 Native 引擎的子驱动程序


  16 /* define or undefine these macros
  17    to include or exclude specific fb driver.
  18  */
  19 #undef _FBLIN1_SUPPORT
  20 // #define _FBLIN1_SUPPORT        1
  21
  22 #undef _FBLIN2_SUPPORT
  23 // #define _FBLIN2_SUPPORT        1
  24
  22 #undef _FBLIN_2_SUPPORT
  23 // #define _FBLIN_2_SUPPORT        1
  24
  25 //#undef _FBLIN4_SUPPORT
  26 #define _FBLIN4_SUPPORT        1
  27
  28 // #undef _FBLIN8_SUPPORT
  29 #define _FBLIN8_SUPPORT        1
  30
  31 // #undef _FBLIN16_SUPPORT
  32 #define _FBLIN16_SUPPORT       1
  33
  34 #undef _FBLIN24_SUPPORT
  35 // #define _FBLIN24_SUPPORT       1
  36
  37 #undef _FBLIN32_SUPPORT
  38 // #define _FBLIN32_SUPPORT       1
  39
  40 #define HAVETEXTMODE    1       /* =0 for graphics only systems*/

其中,HAVETEXTMODE 定义系统是否有文本模式,可将 MiniGUI 中用来关闭文本模式的代码屏蔽掉。_FBLIN_2_SUPPORT 和_FBLIN2_SUPPORT 分别用来定义 big endian 和 little endian 的 2bpp 驱动程序。

对于输入引擎来说,情况就有些不同了。因为目前还没有统一的处理输出设备的接口,而且每个嵌入式系统的输入设备也各不相同,所以,我们通常要针对特定嵌入式系统重新输入引擎。下面的代码就是针对 ADS 基于 StrongARM 的嵌入式开发系统编写的输入引擎(清单 10):

           清单 10  为 ADS 公司基于 StrongARM 的嵌入式开发系统编写的输入引擎

  30
  31 #include <stdio.h>
  32 #include <stdlib.h>
  33 #include <string.h>
  34 #include <unistd.h>
  35 #include <sys/io.h>
  36 #include <sys/ioctl.h>
  37 #include <sys/poll.h>
  38 #include <linux/kd.h>
  39 #include <sys/types.h>
  40 #include <sys/stat.h>
  41 #include <fcntl.h>
  42
  43 #include "common.h"
  44 #include "misc.h"
  45 #include "ads_internal.h"
  46 #include "ial.h"
  47 #include "ads.h"
  48
  49 #ifndef NR_KEYS
  50 #define NR_KEYS         128
  51 #endif
  52
  53 static int ts;
  54 static int mousex = 0;
  55 static int mousey = 0;
  56 static POS pos;
  57
  58 static unsigned char state[NR_KEYS];
  59
  60 /************************  Low Level Input Operations **********************/
  61 /*
  62  * Mouse operations -- Event
  63  */
  64 static int mouse_update(void)
  65 {
  66         return 0;
  67 }
  68
  69 static int mouse_getx(void)
  70 {
  71         return mousex;
  72 }
  73
  74 static int mouse_gety(void)
  75 {
  76         return mousey;
  77 }
  78
  79 static void mouse_setposition(int x, int y)
  80 {
  81 }
  82
  83 static int mouse_getbutton(void)
  84 {
  85         return pos.b;
  86 }
  87
  88 static void mouse_setrange(int minx,int miny,int maxx,int maxy)
  89 {
  90 }
  91
  92 static int keyboard_update(void)
  93 {
  94         return 0;
  95 }
  96
  97 static char * keyboard_getstate(void)
  98 {
  99         return (char *)state;
100 }
101
102 #ifdef _LITE_VERSION
103 static int wait_event (int which, int maxfd, fd_set *in, fd_set *out, fd_set  *except,
104                 struct timeval *timeout)
105 {
106         fd_set rfds;
107         int     e;
108
109     if (!in) {
110         in = &rfds;
111         FD_ZERO (in);
112     }
113
114         if (which & IAL_MOUSEEVENT) {
115                 FD_SET (ts, in);
116         if (ts > maxfd) maxfd = ts;
117         }
118
119         e = select (maxfd + 1, in, out, except, timeout) ;
120
121         if (e > 0) {
122                 if (ts >= 0 && FD_ISSET (ts, in))
123                 {
124             FD_CLR (ts, in);
125                         read (ts, &pos, sizeof (POS));
126                         if ( pos.x !=-1 && pos.y !=-1) {
127                                 mousex = pos.x;
128                                 mousey = pos.y;
129                         }
130                         pos.b = ( pos.b > 0 ? 4:0);
131                         return IAL_MOUSEEVENT;
132                 }
133
134         } else if (e < 0) {
135                 return -1;
136         }
137         return 0;
138 }
139 #else
140 static int wait_event (int which, fd_set *in, fd_set *out, fd_set *except,
141                 struct timeval *timeout)
142 {
143         struct pollfd ufd;
144         if ( (which & IAL_MOUSEEVENT) == IAL_MOUSEEVENT)
145         {
146                 ufd.fd     = ts;
147                 ufd.events = POLLIN;
148                 if ( poll (&ufd, 1, timeout) > 0)
149                 {
150                         read (ts, &pos, sizeof(POS));
151                         return IAL_MOUSEEVENT;
152                 }
153         }
154         return 0;
155 }
156 #endif
157
158 static void set_leds (unsigned int leds)
159 {
160 }
161
162 BOOL InitADSInput (INPUT* input, const char* mdev, const char* mtype)
163 {
164         int i;
165
166         ts = open ("/dev/ts", O_RDONLY);
167     if ( ts < 0 ) {
168         fprintf (stderr, "IAL: Can not open touch screen!\n");
169         return FALSE;
170     }
171
172         for(i = 0; i < NR_KEYS; i++)
173                 state[i] = 0;
174
175     input->update_mouse = mouse_update;
176     input->get_mouse_x = mouse_getx;
177     input->get_mouse_y = mouse_gety;
178     input->set_mouse_xy = mouse_setposition;
179     input->get_mouse_button = mouse_getbutton;
180     input->set_mouse_range = mouse_setrange;
181
182     input->update_keyboard = keyboard_update;
183     input->get_keyboard_state = keyboard_getstate;
184     input->set_leds = set_leds;
185
186     input->wait_event = wait_event;
187         mousex = 0;
188         mousey = 0;
189     return TRUE;
190 }
191
192 void TermADSInput (void)
193 {
194         if ( ts >= 0 )
195                 close(ts);
196 }
197

在上述输入引擎中,完全忽略了键盘相关的函数实现,代码集中在对触摸屏的处理上。显然,输入引擎的编写并不是非常困难的。

[目录]


小结

6 小结

本文详细介绍了 MiniGUI 的 GAL 和 IAL 接口,并以 Native 图形引擎和输入引擎为例,介绍了具体图形引擎和输入引擎的实现。当然,MiniGUI 目前的 GAL 和 IAL 接口还有许多不足之处,比如和上层的 GDI 耦合程度不高,从而对效率有些损失。在 MiniGUI 将来的开发中,我们将重新设计 GDI 以及底层的图形引擎接口,以便针对窗口系统进行优化。

[目录]


开发指南

    自 MiniGUI 从 1998 年底推出以来,越来越多的人开始选择 MiniGUI 在 Linux 上开发实时嵌入式系统。为了帮助嵌入式软件开发人员使用 MiniGUI编写出更好的应用程序,我们将撰写一系列文章讲解基于 Linux 和 MiniGUI 的嵌入式系统软件开发,并冠名"基于 Linux 和 MiniGUI 的嵌入式系统软件开发指南"。
[目录]


选择MiniGUI-Threads或者MiniGUI-Lite

1:引言
    自 MiniGUI 从 1998 年底推出以来,越来越多的人开始选择 MiniGUI 在 Linux 上开发实时嵌入式系统。MiniGUI 系统也逐渐成熟,并在各种嵌入式系统中扮演了重要的角色。为了帮助嵌入式软件开发人员使用 MiniGUI编写出更好的应用程序,我们将撰写一系列文章讲解基于 Linux 和 MiniGUI 的嵌入式系统软件开发,并冠名"基于 Linux 和 MiniGUI 的嵌入式系统软件开发指南"。该系列文章将讲述如何在基于 Linux 的系统上利用 MiniGUI 开发具有图形用户界面支持的嵌入式系统软件,其内容不仅仅限于 MiniGUI 的编程,还会涉及到一些 Linux 下嵌入式系统软件开发的技巧。系列文章的初步规划如下:

    ·如何针对特定项目选择 MiniGUI-Threads 和 MiniGUI-Lite
    ·理解消息循环和窗口过程
    ·对话框和控件编程
    ·使用 GDI 函数
    ·MiniGUI 和 Linux 系统调用
    ·MiniGUI-Lite 与进程间通讯
    ·将 MiniGUI 及应用程序移植到特定平台
    ·利用 autoconf 接口编写跨平台代码
    ·如何调试 MiniGUI 应用程序

    本文是该系列文章的第一篇,将讲述如何针对具体项目选择使用 MiniGUI-Threads 或者 MiniGUI-Lite 版本,并比较不同版本对系统软件结构的影响。


2:MiniGUI-Threads 和 MiniGUI-Lite 的区别
    大家都知道,我们可以将 MiniGUI 编译成两个截然不同的版本,一个是 MiniGUI-Threads,一个是 MiniGUI-Lite。这两个版本适用于不同的应用需求。在选择到底使用 MiniGUI-Threads 还是 MiniGUI-Lite 之前,我们首先需要了解这两个版本之间的区别。

    MiniGUI-Threads 是 MiniGUI 的最初版本。MiniGUI 最初为一个工业控制系统开发的,该系统功能单一,但却需要非常高的实时性,因此考虑将 MiniGUI 开发成一个基于多线程的图形用户界面支持系统。因为在传统的 UNIX/Linux 系统上,典型的 GUI 系统(比如 X)采用传统的基于 UNIX 套接字的客户/服务器系统结构。在这种体系结构下,客户建立窗口、绘制等等都要通过套接字传递到服务器,由服务器完成实质工作。这样,系统非常依赖于 UNIX 套接字通讯。而大家都知道,UNIX 套接字的数据传递,要经过内核,然后再传递到另外一个程序。这样,大量的数据在客户/内核/服务器之间传递,从而增加了系统负荷,也占用了许多系统资源。这对许多嵌入式系统,尤其是实时性要求非常高的系统来说,是不可接受的。

    为了解决这个问题,MiniGUI 首先采用了线程机制(类似Windows CE),所有的应用程序都运行在同一个地址空间,这样,大大提高了程序之间的通讯效率,并且特别适合于实时性要求非常高的系统。这就是 MiniGUI-Threads。基于 MiniGUI-Threads 的程序,可以具有多个线程,每个线程有不同的功能和任务,并且可以建立各自的窗口,不同的线程之间,可以通过 MiniGUI 提供的消息传递机制进行事件传送和同步。

    但显然,这种基于线程的结构也导致了系统整体的脆弱――如果某个线程因为非法的数据访问而终止运行,则整个进程都将受到影响。不过,这种体系结构对实时控制系统等时间关键的系统来讲,还是非常适合的。

    为了解决 MiniGUI-Threads 版本因为线程而引入的一些问题,同时也为了让 MiniGUI更加适合于嵌入式系统,我们决定开发一个 MiniGUI-Lite 版本。这个版本的开发目的是:

    ·保持与原先 MiniGUI 版本在源代码级 99% 以上的兼容。
    ·不再使用线程库。
    ·可以同时运行多个基于 MiniGUI-Lite 的应用程序,即多个进程,并且提供前后台进程的切换。

    显然,要同时满足上述三个目的,如果采用传统的 C/S 结构对MiniGUI-Threads 进行改造,应该不难实现。但前面提到的传统 C/S 结构的缺陷却无法避免。经过对 PDA 等嵌入式系统的分析,我们发现,某些 PDA 产品具有运行多个任务的能力,但同一时刻在屏幕上进行绘制的程序,一般不会超过两个。因此,只要确保将这两个进程的绘制相互隔离,就不需要采用复杂的 C/S 结构处理多个进程窗口之间的互相剪切。也就是说,在这种产品中,如果采用基于传统 C/S 结构的多窗口系统,实际是一种浪费。

    有了上述认识,我们对 MiniGUI-Threads 进行了如下简化设计:

    每个进程维护自己的主窗口 Z 序,同一进程创建的主窗口之间互相剪切。也就是说,除这个进程只有一个线程,只有一个消息循环之外,它与原有的 MiniGUI 版本之间没有任何区别。每个进程在进行屏幕绘制时,不需要考虑其他进程。
    建立一个简单的客户/服务器体系,但确保最小化进程间的数据复制功能。因此,在服务器和客户之间传递的数据仅限于输入设备的输入数据,以及客户和服务器之间的某些请求和响应数据。
有一个服务器进程(mginit),它负责初始化一些输入设备,并且通过 UNIX Domain 套接字将输入设备的消息发送到前台的 MiniGUI-Lite 客户进程。
    服务器和客户被分别限定在屏幕的某两个不相交矩形内进行绘制,同一时刻,只能有一个客户及服务器进行屏幕绘制。其他客户可继续运行,但屏幕输入被屏蔽。服务器可以利用 API 接口将某个客户切换到前台。同时,服务器和客户之间采用信号和 System V 信号量进行同步。
    服务器还采用 System V IPC 机制提供一些资源的共享,包括位图、图标、鼠标、字体等等,以便减少实际内存的消耗。

    从传统 C/S 窗口系统的角度看,MiniGUI-Lite 的这种设计,无法处理达到完整的多窗口支持,这的确是一个结构设计上的不足和缺陷。不过,这实际是 MiniGUI-Lite 不同于其他窗口系统的一个特征。因为处理每个进程之间的互相剪切问题,将导致客户和服务器之间的通讯量大大增加,但实际上在许多嵌入式系统当中这种处理是没有必要的。在类似 PDA 的嵌入式系统中,往往各个程序启动后,就独占屏幕进行绘制输出,其他程序根本就没有必要知道它现在的窗口被别的进程剪切了,因为它根本就没有机会输出到屏幕上。所以,在 MiniGUI-Lite 当中,当一个进程成为最顶层程序时,服务器会保证其输出正常,而当有新的程序成为最顶层程序时,服务器也会保证其他程序不能输出到屏幕上。但这些进程依然在正常执行着,不过,服务器只向最顶层的程序发送外部事件消息。

    表 1 给出了 MiniGUI-Threads 和 MiniGUI-Lite 的区别。从表中总结的区别看来,MiniGUI-Threads 适合于功能单一、实时性要求很高的系统,比如工业控制系统;而 MiniGUI-Lite 适合于功能丰富、结构复杂、显示屏幕较小的系统,比如 PDA 等信息产品。

表 1 MiniGUI-Threads 和 MiniGUI-Lite 的区别

# MiniGUI-Threads
* MiniGUI-Lite

多窗口支持
    # 完全
    * 不能处理进程间窗口的剪切,但提供进程内多窗口的完全支持

字体支持
    # 支持点阵字体(VBF、RBF)和矢量字体(Adobe Type1 和 TrueType)
    * 目前尚不支持对 Adobe Type1 和 TrueType 等矢量字体的支持

线程间消息传递
    # 通过 MiniGUI 的消息函数,可在不同的线程之间传递消息
    * 未考虑多线程应用,不能直接通过 MiniGUI 消息函数在不同线程之间传递消息

多线程窗口
    # MiniGUI 能够处理不同线程之间的窗口层叠
    * 不能处理多线程之间的窗口层叠

其他
    # 基于线程的 C/S 结构,系统健壮性较差,因此要求系统经过严格测试
    * 采用 UNIX Domain Socket 的基于进程的 C/S 结构,可建立健壮的软件架构。并提供了方便的高层 IPC 机制

    除上表中列出的不同之外,MiniGUI-Threads 和 MiniGUI-Lite 的 API 是一致的。

3:MiniGUI-Threads 的典型应用和软件架构
    本文介绍的基于 MiniGUI-Threads 典型应用是一个计算机数字控制(CNC)系统。这个系统是由清华大学基于 RT-Linux 建立的机床控制系统。该系统使用 MiniGUI-Threads 作为图形用户界面支持系统。图 1 是该 CNC 系统的用户界面。

图 1 清华大学基于 RT-Linux 和 MiniGUI 的数控系统主界面

    图 2 是该系统的架构。在用户层,该系统有三个线程,一个作为 GUI 主线程存在,另一个作为监视线程监视系统的工作状态,并在该线程建立的窗口上输出状态信息,第三个线程是工作线程,该线程执行加工指令,并通过 RT-Linux 的实时 FIFO 和系统的实时模块进行通讯。

图 2 清华大学基于 RT-Linux 和 MiniGUI 的数控系统架构

4:MiniGUI-Lite 的典型应用和软件架构
    这里介绍的典型应用是一个基于 MiniGUI-Lite 的 PDA。该 PDA 由国内某公司基于 Linux 开发,其上可以运行各种 PIM 程序、浏览器以及各种游戏程序。图 3 是该 PDA 的用户界面。

图 3 某公司开发的基于 MiniGUI 的 PDA 软件界面

    该系统中的所有应用程序都以 Linux 进程的形式执行,mginit(即 MiniGUI-Lite)提供了输入法支持和应用程序管理功能。当应用程序之间需要通讯时,可以通过 MiniGUI-Lite 所提供的 request/response 接口实现。图 4 是该系统的架构。

图 4 某公司开发的基于 MiniGUI 的 PDA 软件架构

5:小结
    本文讲解了 MiniGUI-Threads 和 MiniGUI-Lite 之间的区别,并举例说明了基于这两个不同版本的不同软件架构。嵌入式程序开发人员必须明白这两个版本之间的区别,并针对具体应用恰当选择使用哪个版本。

[目录]


消息循环和窗口过程

引言
    我们知道,流行的 GUI 编程都有一个重要的概念与之相关,即"事件驱动编程"。事件驱动的含义就是,程序的流程不再是只有一个入口和若干个出口的串行执行线路;相反,程序会一直处于一个循环状态,在这个循环当中,程序从外部输入设备获取某些事件,比如用户的按键或者鼠标的移动,然后根据这些事件作出某种的响应,并完成一定的功能,这个循环直到程序接受到某个消息为止。"事件驱动"的底层设施,就是常说的"消息队列"和"消息循环"。本文将具体描述 MiniGUI 中用来处理消息的几个重要函数,并描述 MiniGUI-Threads 和 MiniGUI-Lite 在消息循环实现上的一些不同。

    窗口是 MiniGUI 当中最基本的 GUI 元素,一旦窗口建立之后,窗口就会从消息队列当中获取属于自己的消息,然后交由它的窗口过程进行处理。这些消息当中,有一些是基本的输入设备事件,而有一些则是与窗口管理相关的逻辑消息。本文将讲述 MiniGUI 中的窗口建立和销毁过程,并解释了窗口过程的概念以及对一些重要消息的处理。

2 消息和消息循环
    在 MiniGUI 中,消息被如下定义(include/window.h):

352 typedef struct _MSG
353 {
354     HWND             hwnd;
355     int              message;
356     WPARAM           wParam;
357     LPARAM           lParam;
358 #ifdef _LITE_VERSION
359     unsigned int     time;
360 #else
361     struct timeval   time;
362 #endif
363     POINT            pt;
364 #ifndef _LITE_VERSION
365     void*            pAdd;
366 #endif
367 }MSG;
368 typedef MSG* PMSG;

    一个消息由该消息所属的窗口(hwnd)、消息编号(message)、消息的 WPARAM 型参数(wParam)以及消息的 LPARAM 型参数(lParam)组成。消息的两个参数中包含了重要的内容。比如,对鼠标消息而言,lParam 中一般包含鼠标的位置信息,而 wParam 参数中则包含发生该消息时,对应的 SHIFT 键的状态信息等。对其他不同的消息类型来讲,wParam 和 lParam 也具有明确的定义。当然,用户也可以自定义消息,并定义消息的 wParam 和 lParam 意义。为了用户能够自定义消息,MiniGUI 定义了 MSG_USER 宏,可如下定义自己的消息:

#define MSG_MYMESSAGE1  (MSG_USER + 1)
#define MSG_MYMESSAGE2  (MSG_USER + 2)

    用户可以在自己的程序中使用自定义消息,并利用自定义消息传递数据。

    在理解消息之后,我们看消息循环。简而言之,消息循环就是一个循环体,在这个循环体中,程序利用 GetMessage 函数不停地从消息队列中获得消息,然后利用 DispatchMessage 函数将消息发送到指定的窗口,也就是调用指定窗口的窗口过程,并传递消息及其参数。典型的消息循环如下所示:

while (GetMessage (&Msg, hMainWnd)) {
       TranslateMessage (&Msg);
       DispatchMessage (&Msg);
}

    如上所示,GetMessage 函数从 hMainWnd 窗口所属的消息队列当中获得消息,然后调用 TranslateMessage 函数将 MSG_KEYDOWN 和 MSG_KEYUP 消息翻译成 MSG_CHAR 消息,最后调用 DispatchMessage 函数将消息发送到指定的窗口。

    在 MiniGUI-Threads 版本中,每个建立有窗口的 GUI 线程有自己的消息队列,而且,所有属于同一线程的窗口共享同一个消息队列。因此,GetMessage 函数将获得所有与 hMainWnd 窗口在同一线程中的窗口的消息。

    而在 MiniGUI-Lite 版本中,只有一个消息队列,GetMessage 将从该消息队列当中获得所有的消息,而忽略 hMainWnd 参数。

3 几个重要的消息处理函数
    除了上面提到的 GetMessage 和 TranslateMessage、DispatchMessage 函数以外,MiniGUI 支持如下几个消息处理函数。

    PostMessage:该函数将消息放到指定窗口的消息队列后立即返回。这种发送方式称为"邮寄"消息。如果消息队列中的邮寄消息缓冲区已满,则该函数返回错误值。在下一个消息循环中,由 GetMessage 函数获得这个消息之后,窗口才会处理该消息。PostMessage 一般用于发送一些非关键性的消息。比如在 MiniGUI 中,鼠标和键盘消息就是通过 PostMessage 函数发送的。

    SendMessage:该函数和 PostMessage 函数不同,它在发送一条消息给指定窗口时,将等待该消息被处理之后才会返回。当需要知道某个消息的处理结果时,使用该函数发送消息,然后根据其返回值进行处理。在 MiniGUI-Threads 当中,如果发送消息的线程和接收消息的线程不是同一个线程,发送消息的线程将阻塞并等待另一个线程的处理结果,然后继续运行;否则,SendMessage 函数将直接调用接收消息窗口的窗口过程函数。MiniGUI-Lite 则和上面的第二种情况一样,直接调用接收消息窗口的窗口过程函数。

    SendNotifyMessage:该函数和 PostMessage 消息类似,也是不等待消息被处理即返回。但和 PostMessage 消息不同,通过该函数发送的消息不会因为缓冲区满而丢失,因为系统采用链表的形式处理这种消息。通过该函数发送的消息一般称为"通知消息",一般用来从控件向其父窗口发送通知消息。

    PostQuitMessage:该消息在消息队列中设置一个 QS_QUIT 标志。GetMessage 在从指定消息队列中获取消息时,会检查该标志,如果有 QS_QUIT 标志,GetMessage 消息将返回 FALSE,从而可以利用该返回值终止消息循环。

4 MiniGUI-Threads 和 MiniGUI-Lite 在消息处理上的不同
表 1 总结了 MiniGUI-Threads 和 MiniGUI-Lite 在消息处理上的不同

表 1 MiniGUI-Threads 和 MiniGUI-Lite 在消息处理上的不同

# MiniGUI-Threads
* MiniGUI-Lite

多消息队列
    # 每个创建窗口的线程拥有独立的消息队列
    * 只有一个消息队列。所有窗口共享一个消息队列。除非嵌套消息循环,否则一个程序中只有一个消息循环。

内建多线程处理
    # 是。可以自动处理跨线程的消息传递
    * 不能。从一个线程向另外一个线程发送或者邮寄消息时,必须通过互斥处理保护消息队列。

其他
    # 可以利用 PostSyncMessage 函数跨线程发送消息,并等待消息的处理结果
    * 不能使用 PostSyncMessage、SendAsynMessage 等消息。

5 窗口的建立和销毁

5.1 窗口的建立
    我们知道,MiniGUI 的 API 类似 Win32 的 API。因此,窗口的建立过程和 Windows 程序基本类似。不过也有一些差别。首先我们回顾一下 Windows 应用程序的框架:

    ·在 WinMain () 中创建窗口,使用以下步骤:创建窗口类、登记窗口类、创建并显示窗口、启动消息循环。
    ·在 WndProc () 中,负责对发到窗口中的各种消息进行响应。
    ·在 MiniGUI 中也同样要有这两个函数。不过稍微有点不同。程序的入口函数名字叫MiniGUIMain (),它负责创建程序的主窗口。在建立主窗口之后,程序进入消息循环。

    在 Win32 程序中,在建立一个主窗口之前,程序首先要注册一个窗口类,然后创建一个属于该窗口类的主窗口。MiniGUI 却没有在主窗口中使用窗口类的概念。在 MiniGUI 程序中,首先初始化一个 MAINWINCREATE 结构,该结构中元素的含义是:

CreateInfo.dwStyle: 窗口风格
CreateInfo.spCaption: 窗口的标题
CreateInfo.dwExStyle : 窗口的附加风格
CreateInfo.hMenu: 附加在窗口上的菜单句柄
CreateInfo.hCursor: 在窗口中所使用的鼠标光标句柄
CreateInfo.hIcon: 程序的图标
CreateInfo.MainWindowProc: 该窗口的消息处理函数指针
CreateInfo.lx: 窗口左上角相对屏幕的绝对横坐标,以象素点表示
CreateInfo.ty: 窗口左上角相对屏幕的绝对纵坐标,以象素点表示
CreateInfo.rx: 窗口的长,以象素点表示
CreateInfo.by: 窗口的高,以象素点表示
CreateInfo.iBkColor: 窗口背景颜色
CreateInfo.dwAddData: 附带给窗口的一个 32 位值
CreateInfo.hHosting: 窗口消息队列所属

其中有如下几点要特别说明:

    CreateInfo.dwAddData:在程序编制过程中,应该尽量减少静态变量,但是如何不使用静态变量而给窗口传递参数呢?这时可以使用这个域。该域是一个 32 位的值,因此可以把所有需要传递给窗口的参数编制成一个结构,而将结构的指针赋予该域。在窗口过程中,可以使用 GetWindowAdditionalData 函数获取该指针,从而获得所需要传递的参数。
    CreateInfo.hHosting:该域表示的是将要建立的主窗口使用哪个主窗口的消息队列。使用其他主窗口消息队列的主窗口,我们称为"被托管"的主窗口。当然,这只在 MiniGUI-Threads 版本中有效。
MainWinProc 函数负责处理窗口消息。这个函数就是主窗口的"窗口过程"。窗口过程一般有四个入口参数,第一个是窗口句柄,第二个是消息类型,第三个和第四个是消息的两个参数。

    在准备好MAINWINCREATE 结构之后,就可以调用 CreateMainWindow 函数建立主窗口了。在建立主窗口之后,典型的程序将进入消息循环。如下所示:

int MiniGUIMain (int args, const char* arg[])
{
    MSG Msg;
    MAINWINCREATE CreateInfo;
    HWND hWnd;

    // 初始化 MAINWINCREATE 结构
    CreateInfo.dwStyle = WS_VISIBLE | WS_VSCROLL | WS_HSCROLL | WS_CAPTION;
    CreateInfo.spCaption= "MiniGUI step three";
    CreateInfo.dwExStyle = WS_EX_NONE;
    CreateInfo.hMenu = createmenu();
    CreateInfo.hCursor = GetSystemCursor(0);
    CreateInfo.hIcon = 0;
    CreateInfo.MainWindowProc = MainWinProc;
    CreateInfo.lx = 0;
    CreateInfo.ty = 0;
    CreateInfo.rx = 640;
    CreateInfo.by = 480;
    CreateInfo.iBkColor = COLOR_lightwhite;
    CreateInfo.dwAddData = 0;
    CreateInfo.hHosting = HWND_DESKTOP;

    // 建立主窗口
    hWnd = CreateMainWindow(&CreateInfo);
    if (hWnd == HWND_INVALID)
        return 0;

    // 显示主窗口
    ShowWindow (hWnd, SW_SHOWNORMAL);

    //  进入消息循环
    while (GetMessage(&Msg, hWnd)) {
        TranslateMessage (&Msg);
        DispatchMessage(&Msg);
    }

    MainWindowThreadCleanup (hWnd);
    return 0;
}

    注意,和 Windows 程序不同的是,在退出消息循环之后,还要调用一个函数,即 MainWindowThreadCleaup 函数。该函数的工作是销毁主窗口的消息队列,一般在线程或者进程的最后调用。

5.2 窗口的销毁
    要销毁一个主窗口,可以利用 DestroyMainWindow (hWnd) 函数。该函数将销毁主窗口,但不会销毁主窗口所使用的消息队列,而要使用MainWindowThreadCleaup 最终清除主窗口所使用的消息队列。

    一般而言,一个主窗口过程在接收到 MSG_CLOSE 消息之后会销毁主窗口,并调用 PostQuitMessage 消息终止消息循环。如下所示:

case MSG_CLOSE:
    // 销毁窗口使用的资源
    DestroyLogFont (logfont1);
    DestroyLogFont (logfont2);
    DestroyLogFont (logfont3);

    // 销毁子窗口
    DestroyWindow(hWndButton);
    DestroyWindow(hWndEdit);
    // 销毁主窗口
    DestroyMainWindow (hWnd);
    // 发送 MSG_QUIT 消息
    PostQuitMessage(hWnd);
    return 0;


6 几个重要消息
    在窗口(包括主窗口和子窗口在内)的生存周期当中,有几个重要的消息需要仔细处理。下面描述这些消息的概念和典型处理。

6.1 MSG_NCCREATE
    该消息在 MiniGUI 建立主窗口的过程中发送到窗口过程。lParam 中包含了由 CreateMainWindow 传递进入的 pCreateInfo 结构指针。您可以在该消息的处理过程中修改 pCreateInfo 结构中的某些值。

6.2 MSG_SIZECHANGING
    该消息窗口尺寸发生变化时,或者建立窗口时发送到窗口过程,用来确定窗口大小。wParam 包含预期的窗口尺寸值,而 lParam 用来保存结果值。MiniGUI 的默认处理是,

case MSG_SIZECHANGING:
    memcpy ((PRECT)lParam, (PRECT)wParam, sizeof (RECT));
    return 0;

    你可以截获该消息的处理,从而让即将创建的窗口位于指定的位置,或者具有固定的大小,比如在 SPINBOX 控件中,就处理了该消息,使之具有固定的大小:

case MSG_SIZECHANGING:
{
    const RECT* rcExpect = (const RECT*) wParam;
    RECT* rcResult = (RECT*) lPraram;

    rcResult->left = rcExpect->left;
    rcResult->top = rcExpect->top;
    rcResult->right = rcExpect->left +  _WIDTH;
    rcResult->bottom = rcExpect->left +  _HEIGHT;
    return 0;
}

6.3 MSG_CHANGESIZE
    在确立窗口大小之后,该消息被发送到窗口过程,用来通知确定之后的窗口大小。wParam 包含了窗口大小 RECT 的指针。注意应用程序应该将该消息传递给 MiniGUI 进行默认处理。

6.4 MSG_SIZECHANGED
    该消息用来确定窗口客户区的大小,和 MSG_SIZECHANGING 消息类似。wParam 参数包含窗口大小信息,lParam 参数是用来保存窗口客户区大小的 RECT 指针,并且具有默认值。如果该消息的处理返回非零值,则将采用 lParam 当中包含的大小值作为客户区的大小;否则,将忽略该消息的处理。比如在 SPINBOX 控件中,就处理了该消息,并使客户区占具所有的窗口范围:

case MSG_SIZECHANGED
{
    RECT* rcClient = (RECT*) lPraram;

    rcClient->right = rcClient->left  +  _WIDTH;
    rcClient->bottom = rcClient->top +  _HEIGHT;
    return 0;
}

6.5 MSG_CREATE
    该消息在建立好的窗口成功添加到 MiniGUI 的窗口管理器之后发送到窗口过程。这时,应用程序可以在其中创建子窗口。如果该消息返回非零值,则将销毁新建的窗口。注意,在 MSG_NCCREATE 消息被发送时,窗口尚未正常建立,所以不能在 MSG_NCCREATE 消息中建立子窗口。

6.6 MSG_PAINT
    该消息在需要进行窗口重绘时发送到窗口过程。MiniGUI 通过判断窗口是否含有无效区域来确定是否需要重绘。当窗口在初始显示、从隐藏状态变化为显示状态、从部分不可见到可见状态,或者应用程序调用 InvalidateRect 函数使某个矩形区域变成无效时,窗口将具有特定的无效区域。这时,MiniGUI 将在处理完所有的邮寄消息、通知消息之后处理无效区域,并向窗口过程发送 MSG_PAINT 消息。该消息的典型处理如下:

case MSG_PAINT:
{
    HDC hdc;

    hdc = BeginPaint (hWnd);

    // 使用 hdc 绘制窗口
    ...

    EndPaint (hWnd, hdc);
    break;
}

6.7 MSG_DESTROY
    该消息在应用程序调用 DestroyMainWindow 或者 DestroyWindow 时发送到窗口过程当中,用来通知系统即将销毁一个窗口。如果该消息的处理返回非零值,则将取消销毁过程。

7 Hello, World
    在这个小节当中,我们给出一个简单的示例程序,该程序在窗口中打印"Hello, world!":

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <minigui/common.h>
#include <minigui/minigui.h>
#include <minigui/gdi.h>
#include <minigui/window.h>

static int HelloWinProc (HWND hWnd, int message, WPARAM wParam, LPARAM lParam)
{
    HDC hdc;

    switch (message) {
        case MSG_PAINT:
            hdc = BeginPaint (hWnd);
            TexOut (hdc, 0, 0, "Hello, world!");
            EndPaint (hWnd, hdc);
        break;

        case MSG_CLOSE:
            DestroyMainWindow (hWnd);
            PostQuitMessage (hWnd);
        return 0;
    }

    return DefaultMainWinProc(hWnd, message, wParam, lParam);
}

static void InitCreateInfo (PMAINWINCREATE pCreateInfo)
{
    pCreateInfo->dwStyle = WS_CAPTION | WS_VISIBLE;
    pCreateInfo->dwExStyle = 0;
    pCreateInfo->spCaption = "Hello, world!" ;
    pCreateInfo->hMenu = 0;
    pCreateInfo->hCursor = GetSystemCursor (0);
    pCreateInfo->hIcon = 0;
    pCreateInfo->MainWindowProc = HelloWinProc;
    pCreateInfo->lx = 0;
    pCreateInfo->ty = 0;
    pCreateInfo->rx = 320;
    pCreateInfo->by = 240;
    pCreateInfo->iBkColor = PIXEL_lightwhite;
    pCreateInfo->dwAddData = 0;
    pCreateInfo->hHosting = HWND_DESKTOP;
}

int MiniGUIMain (int args, const char* arg[])
{
    MSG Msg;
    MAINWINCREATE CreateInfo;
    HWND hMainWnd;

#ifdef _LITE_VERSION
    SetDesktopRect (0, 0, 800, 600);
#endif

    InitCreateInfo (&CreateInfo);

    hMainWnd = CreateMainWindow (&CreateInfo);
    if (hMainWnd == HWND_INVALID)
        return -1;

    while (GetMessage (&Msg, hMainWnd)) {
        DispatchMessage (&Msg);
    }

    MainWindowThreadCleanup (hMainWnd);
    return 0;
}

    很显然,这是一个非常简单的程序。该程序使用了 MiniGUI 的默认过程来处理我们前面提到的许多消息,而仅仅处理了 MSG_PAINT 和 MSG_CLOSE 两条消息。当用户单击标题栏上的关闭按钮时,MiniGUI 将发送 MSG_CLOSE 到窗口过程。这时,应用程序就可以销毁窗口,并终止消息循环,最终退出程序。

8 小结
    本文描述了 MiniGUI 中与消息相关的几个重要函数,并讲述了 MiniGUI-Threads 和 MiniGUI-Lite 在消息机制实现上的几个不同。本文还讲述了在 MiniGUI 中的窗口建立和销毁过程,并解释了窗口过程的概念以及一些重要消息的处理。最后,本文给出了一个简单的 MiniGUI 的示例程序,该程序建立窗口,并在其中打印"Hello, world!"。


[目录]


对话框和控件编程

1 引言
    对话框编程是一个快速构建用户界面的技术。通常,我们编写简单的图形用户界面时,可以通过调用 CreateWindow 函数直接创建所有需要的子窗口,即控件。但在图形用户界面比较复杂的情况下,每建立一个控件就调用一次 CreateWindow 函数,并传递许多复杂参数的方法很不可取。主要原因之一,就是程序代码和用来建立控件的数据混在一起,不利于维护。为此,一般的 GUI 系统都会提供一种机制,利用这种机制,通过指定一个模板,GUI 系统就可以根据此模板建立相应的主窗口和控件。MiniGUI 也提供这种方法,通过建立对话框模板,就可以建立模态或者非模态的对话框。

    本文首先讲解组成对话框的基础,即控件的基本概念,然后讲解对话模板的定义,并说明模态和非模态对话框之间的区别以及编程技术。

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

    在 Windows 或 X Window 中,系统会预先定义一些控件类,当利用某个控件类创建控件之后,所有属于这个控件类的控件均会具有相同的行为和显示。利用这些技术,可以确保一致的人机操作界面,而对程序员来讲,可以像搭积木一样地组建图形用户界面。MiniGUI 使用了控件类和控件的概念,并且可以方便地对已有控件进行重载,使得其有一些特殊效果。比如,需要建立一个只允许输入数字的编辑框时,就可以通过重载已有编辑框而实现,而不需要重新编写一个新的控件类。

    如果读者曾经编写过 Windows 应用程序的话,应该记得在建立一个窗口之前,必须确保系统中存在新窗口所对应的窗口类。在 Windows 中,程序所建立的每个窗口,都对应着某种窗口类。这一概念和面向对象编程中的类、对象的关系类似。借用面向对象的术语,Windows 中的每个窗口实际都是某个窗口类的一个实例。在 X Window 编程中,也有类似的概念,比如我们建立的每一个 Widget,实际都是某个 Widget 类的实例。

    这样,如果程序需要建立一个窗口,就首先要确保选择正确的窗口类,因为每个窗口类决定了对应窗口实例的表象和行为。这里的表象指窗口的外观,比如窗口边框宽度,是否有标题栏等等,行为指窗口对用户输入的响应。每一个 GUI 系统都会预定义一些窗口类,常见的有按钮、列表框、滚动条、编辑框等等。如果程序要建立的窗口很特殊,就需要首先注册一个窗口类,然后建立这个窗口类一个实例。这样就大大提高了代码的可重用性。

    在 MiniGUI 中,我们认为主窗口通常是一种比较特殊的窗口。因为主窗口代码的可重用性一般很低,如果按照通常的方式为每个主窗口注册一个窗口类的话,则会导致额外不必要的存储空间,所以我们并没有在主窗口提供窗口类支持。但主窗口中的所有子窗口,即控件,均支持窗口类(控件类)的概念。MiniGUI 提供了常用的预定义控件类,包括按钮(包括单选钮、复选钮)、静态框、列表框、进度条、滑块、编辑框等等。程序也可以定制自己的控件类,注册后再创建对应的实例。表 1 给出了 MiniGUI 预先定义的控件类和相应类名称定义。

表 1 MiniGUI 预定义的控件类和对应类名称

控件类 类名称 宏定义 备注
静态框 "static" CTRL_STATIC
按钮 "button" CTRL_BUTTON
列表框 "listbox" CTRL_LISTBOX
进度条 "progressbar" CTRL_PRORESSBAR
滑块 "trackbar" CTRL_TRACKBAR
单行编辑框 "edit"、"sledit" CTRL_EDIT、CTRL_SLEDIT
多行编辑框 "medit"、"mledit" CTRL_MEDIT、CTRL_MLEDIT
工具条 "toolbar" CTRL_TOOLBAR
菜单按钮 "menubutton" CTRL_MENUBUTTON
树型控件 "treeview" CTRL_TREEVIEW 包含在 mgext 库,即MiniGUI 扩展库中。
月历控件 "monthcalendar" CTRL_MONTHCALENDAR 同上
旋钮控件 "spinbox" CTRL_SPINBOX 同上

    在 MiniGUI 中,通过调用 CreateWindow 函数,可以建立某个控件类的一个实例。控件类既可以是表 1 中预定义 MiniGUI 控件类,也可以是用户自定义的控件类。与 CreateWindow 函数相关的几个函数的原型如下(include/window.h):

904 HWND GUIAPI CreateWindowEx (const char* spClassName, const char* spCaption,
905                   DWORD dwStyle, DWORD dwExStyle, int id,
906                   int x, int y, int w, int h, HWND hParentWnd, DWORD dwAddData);
907 BOOL GUIAPI DestroyWindow (HWND hWnd);
908
909 #define CreateWindow(class_name, caption, style, id, x, y, w, h, parent, add_data) \
910         CreateWindowEx(class_name, caption, style, 0, id, x, y, w, h, parent, add_data)

    CreateWindow 函数建立一个子窗口,即控件。它指定了控件类、控件标题、控件风格,以及窗口的初始位置和大小。该函数同时指定子窗口的父窗口。CreateWindowEx 函数的功能和 CreateWindow 函数一致,不过,可以通过 CreateWindowEx 函数指定控件的扩展风格。DestroyWindow 函数用来销毁用上述两个函数建立的控件或者子窗口。

    清单 1 中的程序,利用预定义控件类创建控件。其中hStaticWnd1 是建立在主窗口 hWnd 中的静态框;hButton1、hButton2、hEdit1、hStaticWnd2则是建立在 hStaicWnd1 内部的几个控件,并作为 hStaticWnd1 的子控件而存在,建立了两个按钮、一个编辑框和一个静态按钮;而 hEdit2 是 hStaicWnd2 的子控件,是 hStaticWnd1 的子子控件。

清单1 利用预定义控件类创建控件

#define IDC_STATIC1     100
#define IDC_STATIC2     150
#define IDC_BUTTON1     110
#define IDC_BUTTON2     120
#define IDC_EDIT1       130
#define IDC_EDIT2       140
int ControlTestWinProc (HWND hWnd, int message, WPARAM wParam, LPARAM lParam)
{
        static HWND hStaticWnd1, hStaticWnd2, hButton1, hButton2, hEdit1, hEdit2;
        switch (message) {
        case MSG_CREATE:
        {
            hStaticWnd1 = CreateWindow (CTRL_STATIC,
                                        "This is a static control",
                                        WS_CHILD | SS_NOTIFY | SS_SIMPLE | WS_VISIBLE | WS_BORDER,
                                        IDC_STATIC1,
                                        10, 10, 180, 300, hWnd, 0);
            hButton1    = CreateWindow (CTRL_BUTTON,
                                        "Button1",
                                        WS_CHILD | BS_PUSHBUTTON | WS_VISIBLE,
                                        IDC_BUTTON1,
                                        20, 20, 80, 20, hStaticWnd1, 0);
            hButton2    = CreateWindow (CTRL_BUTTON,
                                        "Button2",
                                        WS_CHILD | BS_PUSHBUTTON | WS_VISIBLE,
                                        IDC_BUTTON2,
                                        20, 50, 80, 20, hStaticWnd1, 0);
            hEdit1      = CreateWindow (CTRL_EDIT,
                                        "Edit Box 1",
                                        WS_CHILD | WS_VISIBLE | WS_BORDER,
                                        IDC_EDIT1,
                                        20, 80, 100, 24, hStaticWnd1, 0);
            hStaticWnd2 = CreateWindow (CTRL_STATIC,
                                        "This is child static control",
                                        WS_CHILD | SS_NOTIFY | SS_SIMPLE | WS_VISIBLE | WS_BORDER,
                                        IDC_STATIC1,
                                        20, 110, 100, 50, hStaticWnd1, 0);
            hEdit2      = CreateWindow (CTRL_EDIT,
                                        "Edit Box 2",
                                        WS_CHILD | WS_VISIBLE | WS_BORDER,
                                        IDC_EDIT2,
                                        0, 20, 100, 24, hStaticWnd2, 0);
           break;
          }
.......
    }
    return DefaultMainWinProc (hWnd, message, wParam, lParam);
}


    用户也可以通过 RegisterWindowClass 函数注册自己的控件类,并建立该控件类的控件实例。如果程序不再使用某个自定义的控件类,则应该使用 UnregisterWindowClass 函数注销自定义的控件类。上述两个函数以及和窗口类相关函数的原型如下(include/window.h):

897 BOOL GUIAPI RegisterWindowClass (PWNDCLASS pWndClass);
898 BOOL GUIAPI UnregisterWindowClass (const char* szClassName);
899 char* GUIAPI GetClassName (HWND hWnd);
900 BOOL GUIAPI GetWindowClassInfo (PWNDCLASS pWndClass);
901 BOOL GUIAPI SetWindowClassInfo (const WNDCLASS* pWndClass);

    RegisterWindowClass 通过 pWndClass 结构注册一个控件类;UnregisterWindowClass 函数则注销指定的控件类;GetClassName 活得窗口的对应窗口类名称,对主窗口而言,窗口类名称为"MAINWINDOW";GetWindowClassInfo 分别用来获取和指定特定窗口类的属性。

    清单 2 中的程序,定义并注册了一个自己的控件类。该控件用来显示安装程序的步骤信息,MSG_SET_STEP_INFO 消息用来定义该控件中显示的所有步骤信息,包括所有步骤名称及其简单描述。MSG_SET_CURR_STEP 消息用来指定当前步骤,控件将高亮显示当前步骤。

清单2 定义并注册自定义控件类

#define STEP_CTRL_NAME "mystep"
#define MSG_SET_STEP_INFO   (MSG_USER + 1)
#define MSG_SET_CURR_STEP   (MSG_USER + 2)
static int StepControlProc (HWND hwnd,
                int message, WPARAM wParam, LPARAM lParam)
{
    HDC hdc;
    HELPWININFO* info;

    switch (message) {
    case MSG_PAINT:
        hdc = BeginPaint (hwnd);
        /* 获取步骤控件信息 */
        info = (HELPWININFO*)GetWindowAdditionalData (hwnd);
        /* 绘制步骤内容 */
        ......
        EndPaint (hwnd, hdc);
        break;
    /* 控件自定义的消息:用来设置步骤信息 */
    case MSG_SET_STEP_INFO:
        SetWindowAdditionalData (hwnd, (DWORD)lParam);
        InvalidateRect (hwnd, NULL, TRUE);
        break;
    /* 控件自定义的消息:用来设置当前步骤信息 */
    case MSG_SET_CURR_STEP:
        InvalidateRect (hwnd, NULL, FALSE);
        break;
    case MSG_DESTROY:
        break;
    }
    return DefaultControlProc (hwnd, message, wParam, lParam);
}
static BOOL RegisterStepControl ()
{
    int result;
    WNDCLASS StepClass;
    StepClass.spClassName = STEP_CTRL_NAME;
    StepClass.dwStyle     = 0;
    StepClass.hCursor     = GetSystemCursor (IDC_ARROW);
    StepClass.iBkColor    = COLOR_lightwhite;
    StepClass.WinProc     = StepControlProc;
    return RegisterWindowClass (&StepClass);
}
static void UnregisterStepControl ()
{
    UnregisterWindowClass (STEP_CTRL_NAME);
}

3 控件子类化
    采用控件类和控件实例的结构,不仅可以提高代码的可重用性,而且还可以方便地对已有控件类进行扩展。比如,在需要建立一个只允许输入数字的编辑框时,就可以通过重载已有编辑框控件类而实现,而不需要重新编写一个新的控件类。在 MiniGUI 中,这种技术称为子类化或者窗口派生。子类化的方法有三种:

    ·一种是对已经建立的控件实例进行子类化,子类化的结果是只影响这一个控件实例;
    ·一种是对某个控件类进行子类化,将影响其后创建的所有该控件类的控件实例;
    ·最后一种是在某个控件类的基础上新注册一个子类化的控件类,不会影响原有控件类。在 Windows 中,这种技术又称为超类化。

    在 MiniGUI 中,控件的子类化实际是通过替换已有的窗口过程实现的。清单 3 中的代码就通过控件类创建了两个子类化的编辑框,一个只能输入数字,而另一个只能输入字母:

清单 3 控件的子类化

#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 对话框和对话框模板
    在 MiniGUI 中,对话框是一类特殊的主窗口,这种主窗口只关注与用户的交互――向用户提供输出信息,但更多的是用于用户输入。对话框可以理解为子类化之后的主窗口类。它针对对话框的特殊性(即用户交互)进行了特殊设计。比如用户可以使用 TAB 键遍历控件、可以利用 ENTER 键表示默认输入等等。在 MiniGUI 当中,在建立对话框之前,首先需要定义一个对话框模板,该模板中定义了对话框本身的一些属性,比如位置和大小等等,同时定义了对话框中所有控件的初始信息,包括位置、大小、风格等等。在 MiniGUI 中,用两个结构来表示对话框模板(src/window.h):

1172 typedef struct
1173 {
1174     char*       class_name;             // control class
1175     DWORD       dwStyle;                // control style
1176     int         x, y, w, h;             // control position in dialog
1177     int         id;                     // control identifier
1178     const char* caption;                // control caption
1179     DWORD       dwAddData;              // additional data
1180
1181     DWORD       dwExStyle;              // control extended style
1182 } CTRLDATA;
1183 typedef CTRLDATA* PCTRLDATA;
1184
1185 typedef struct
1186 {
1187     DWORD       dwStyle;                // dialog box style
1188     DWORD       dwExStyle;              // dialog box extended style
1189     int         x, y, w, h;             // dialog box position
1190     const char* caption;                // dialog box caption
1191     HICON       hIcon;                  // dialog box icon
1192     HMENU       hMenu;                  // dialog box menu
1193     int         controlnr;              // number of controls
1194     PCTRLDATA   controls;               // poiter to control array
1195     DWORD       dwAddData;              // addtional data, must be zero
1196 } DLGTEMPLATE;
1197 typedef DLGTEMPLATE* PDLGTEMPLATE;
1198

    结构 CTRLDATA 用来定义控件,DLGTEMPLATE 用来定义对话框本身。在程序中,应该首先利用 CTRLDATA 定义对话框中所有的控件,并用数组表示。控件在该数组中的顺序,也就是对话框中用户按 TAB 键时的控件切换顺序。然后定义对话框,指定对话框中的控件数目,并指定 DLGTEMPLATE 结构中的 controls 指针指向定义控件的数组。如清单 4 所示。

清单 4 对话框模板的定义

DLGTEMPLATE DlgInitProgress =
{
    WS_BORDER | WS_CAPTION,
    WS_EX_NONE,
    120, 150, 400, 130,
    "VAM-CNC 正在进行初始化,
    0, 0,
    3, NULL,
    0
};

CTRLDATA CtrlInitProgress [] =
{
    {
        "static",
        WS_VISIBLE | SS_SIMPLE,
        10, 10, 380, 16,
        IDC_PROMPTINFO,
        "正在...",
        0
    },
    {
        "progressbar",
        WS_VISIBLE,
        10, 40, 380, 20,
        IDC_PROGRESS,
        NULL,
        0
    },
    {
        "button",
        WS_TABSTOP | WS_VISIBLE | BS_DEFPUSHBUTTON,
        170, 70, 60, 25,
        IDOK,
        "确定",
        0
    }
};

    在定义了对话框模板数据之后,需要定义对话框的回调函数,并调用DialogBoxIndirectParam 函数建立对话框,如清单 5 所示,所建立的对话框如图 1 所示。

清单 5 定义对话框回调函数,并建立对话框

/* 定义对话框回调函数 */
static int InitDialogBoxProc (HWND hDlg, int message, WPARAM wParam, LPARAM lParam)
{
    switch (message) {
    case MSG_INITDIALOG:
        return 1;

    case MSG_COMMAND:
        switch (wParam) {
        case IDOK:
        case IDCANCEL:
            EndDialog (hDlg, wParam);
            break;
        }
        break;

    }

    return DefaultDialogProc (hDlg, message, wParam, lParam);
}
static void InitDialogBox (HWND hWnd)
{
    /* 将对话框和控件数组关联起来 */
    DlgInitProgress.controls = CtrlInitProgress;

    DialogBoxIndirectParam (&DlgInitProgress, hWnd, InitDialogBoxProc, 0L);
}

图 1 清单 5 程序建立的对话框

DialogBoxIndirectParam 以及相关函数的原型如下:

1203 int  GUIAPI DialogBoxIndirectParam (PDLGTEMPLATE pDlgTemplate,
1204                     HWND hOwner, WNDPROC DlgProc, LPARAM lParam);
1205 BOOL GUIAPI EndDialog (HWND hDlg, int endCode);
1206 void GUIAPI DestroyAllControls (HWND hDlg);

    在 DialogBoxIndirectParam 中,需要指定对话框模板(pDlgTemplate)、对话框的托管主窗口句柄(hOwner)、对话框回调函数地址(DlgProc),以及 ud戕?a]dd?dd /)c dd dd? 秋诂发送到对话框回调函数的。该消息的 lParam 参数包含了由 DialogBoxIndirectParam 函数的第四个参数传递到对话框回调函数的值。用户可以利用该值进行对话框的初始化,或者保存起来以备后用。例如,清单 7 中的程序将 MSG_INITDIALOG 消息的 lParam 参数保存到了对话框窗口句柄的附加数据中,这样可以确保在任何需要的时候,方便地从对话框窗口的附加数据中获取这一数据。

static int DepInfoBoxProc (HWND hDlg, int message, WPARAM wParam, LPARAM lParam)
{
    struct _DepInfo *info;

    switch(message) {
    case MSG_INITDIALOG:
    {
        /* 将对话框参数 lParam 保存为窗口的附加数据,以备后用*/
        info = (struct _DepInfo*)lParam;
        SetWindowAdditionalData2 (hDlg, (DWORD)lParam);
        break;
    }

    case MSG_COMMAND:
    {
        /*  从窗口的附加数据中取出保存的对话框参数 */
        info = (struct _DepInfo*) GetWindowAdditionalData2 (hDlg);

        switch(wParam) {
        case IDOK:
            /* 使用 info 结构中的数据 */
            ......

        case IDCANCEL:
            EndDialog(hDlg,wParam);
            break;
        }
    }
    }

    return DefaultDialogProc (hDlg, message, wParam, lParam);
}

    通常而言,传递到对话框回调函数中的参数是一个结构的指针,该结构包含一些初始化对话框的数据,同时也可以将对话框的输入数据保存下来并传递到对话框之外使用。

6 模态和非模态对话框
    简单而言,模态对话框就是显示之后,用户不能再切换到其他主窗口进行工作的对话框,而只能在关闭之后,才能使用其他的主窗口。MiniGUI 中,使用 DialogBoxIndirectParam 函数建立的对话框就是模态对话框。实际上,该对话框首先根据模板建立对话框,然后禁止其托管主窗口,并在主窗口的 MSG_CREATE 消息中创建控件,并发送 MSG_INITDIALOG 消息给回调函数,最终建立一个新的消息循环,并进入该消息循环,直到程序调用 EndDialog 函数为止。

    实际上,我们也可以在 MiniGUI 中利用对话框模板建立普通的主窗口,即非模态对话框。这时,我们使用 CreateMainWindowIndirect 函数。该函数以及相关函数的原型如下(src/window.h):

1199 HWND GUIAPI CreateMainWindowIndirect (PDLGTEMPLATE pDlgTemplate,
1200                     HWND hOwner, WNDPROC WndProc);
1201 BOOL GUIAPI DestroyMainWindowIndirect (HWND hMainWin);

    使用 CreateMainWindowIndirect 根据对话框模板建立的主窗口和其他类型的普通主窗口没有任何区别。

7 小结
    对话框编程是 MiniGUI 应用开发中使用最为常见的一种技术。通过定义一个对话框模板,就可以自动创建一个具有复杂输入输出界面的对话框。本文讲述了 MiniGUI 中的控件类和控件实例的关系,并举例说明控件子类化的概念及应用;然后讲解了 MiniGUI 对话框的编程技术,包括对话框模板的定义和对话框回调函数的编程;最后说明了模态对话框和非模态对话框之间的区别。

[目录]


使用GDI函数

1 引言
    GUI 系统的一个重要组成部分就是 GDI,即图形设备接口(Graphics Device Interface)。通过 GDI,GUI 程序就可以在计算机屏幕上,或者其他的显示设备上进行图形输出,包括基本绘图和文本输出。本文将详细描述 MiniGUI 中的 GDI 函数,并举例说明重要函数的用法。其中包括:DC 的概念、获取和释放;矩形操作和剪切域操作;基本绘图函数;位图操作函数;逻辑字体操作函数等。

2 图形设备上下文
    在 MiniGUI 中,采用了在 Windows 和 X Window 中普遍采用的图形设备概念。每个图形设备定义了计算机显示屏幕上的一个矩形输出区域。在调用图形输出函数时,均要求指定经初始化的图形设备上下文(Device Context,DC),也称作"设备环境"。从程序员的角度看,一个经过初始化的图形设备上下文确定了其后进行图形输出的一些基本属性,并一直保持这些属性,直到被改变为止。这些属性包括:输出的线条颜色、填充颜色、字体颜色、字体形状等等。而从 GUI 系统角度来讲,一个图形设备上下文所代表的含义就要复杂得多,它起码应该包含如下内容:

    ·该设备上下文本所在设备信息(显示模式、色彩深度、显存布局等等);
    ·该设备上下文所代表的窗口以及该窗口被其他窗口剪切的信息(在 MiniGUI 中,称作"全局剪切域");
    ·该设备上下文的基本操作函数(点、直线、多边形、填充、块操作等),及其上下文信息;
    ·由程序设定的局部信息(绘图属性、映射关系和局部剪切域等)。

所以,从程序员的角度看来,他所关心的仅仅是设备上下文本身的一小部分东西。

2.1 设备上下文的获取和释放
    在 MiniGUI 中,所有绘图相关的函数均需要有一个设备上下文。设备上下文可通过 GetClientDC 和 ReleaseDC 获取和释放。由 GetDC 所获取的设备上下文是针对整个窗口的,而 GetClientDC 所获取的设备上下文是针对窗口客户区,也就是说,前一个函数获得的设备上下文,其坐标原点位于窗口左上角,输出被限定在窗口范围之内;后一个函数获得的设备上下文,其坐标原点位于窗口客户区左上角,输出被限定在窗口客户区范围之内。下面是这三个函数的原型说明(include/gdi.h):

398 HDC GUIAPI GetDC (HWND hwnd);
399 HDC GUIAPI GetClientDC (HWND hwnd);
400 void GUIAPI ReleaseDC (HDC hdc);

    GetDC 和 GetClientDC 是从系统预留的若干个 DC 当中获得一个目前尚未使用的设备上下文。所以,应该注意如下两点:

    在使用完成一个由 GetDC 返回的设备上下文之后,应该尽快调用 ReleaseDC 释放。
避免同时使用多个设备上下文,并避免在递归函数中调用 GetDC 和 GetClientDC。

    为了方便程序编写,提高绘图效率,MiniGUI 还提供了建立私有设备上下文的函数,所建立的设备上下文在整个窗口生存期内有效,从而免除了获取和释放的过程。这些函数的原型如下:

403 HDC GUIAPI CreatePrivateDC (HWND hwnd);
404 HDC GUIAPI CreatePrivateClientDC (HWND hwnd);
405 HDC GUIAPI GetPrivateClientDC (HWND hwnd);
406 void GUIAPI DeletePrivateDC (HDC hdc);

    在建立主窗口时,如果主窗口的扩展风格中指定了 WS_EX_USEPRIVATEDC 风格,则 CreateMainWindow 函数会自动为该窗口的客户区建立私有设备上下文。通过 GetPrivateClientDC 函数,可以获得该设备上下文。对控件而言,如果控件类具有 CS_OWNDC 属性,则所有属于该控件类的控件将自动建立私有设备上下文。DeletePrivateDC 函数用来删除私有设备上下文。对上述两种情况,系统将在销毁窗口时自动调用 DeletePrivateDC 函数。

    另外一个获取和释放设备上下文的方法是通过 BeginPaint 和 EndPaint 函数。这两个函数只能在处理 MSG_PAINT 的消息中调用。MiniGUI 在 BeginPaint 函数中通过 GetClientDC 获取客户区设备上下文,然后将窗口当前的无效区域选择到窗口的剪切区域中;而 EndPaint 函数则清空窗口的无效区域,并释放设备上下文。这两个函数的原型如下(include/window.h):

623 HDC GUIAPI BeginPaint(HWND hWnd);
624 void GUIAPI EndPaint(HWND hWnd, HDC hdc);

    因为 BeginPaint 函数将窗口的无效区域选择到了设备上下文中,所以,可以通过一些必要的优化来提高 MSG_PAINT 消息的处理效率。比如,某个程序要在窗口客户区中填充若干矩形,就可以在 MSG_PAINT 函数中如下处理:

MSG_PAINT:
{
    HDC hdc = BeginPaint (hWnd);

    for (j = 0; j < 10; j ++) {
        if (RectVisible  (hdc, rcs + j)) {
            FillBox (hdc, rcs[j].left, rcs[j].top, rcs [j].right, rcs [j].bottom);
        }
    }

    EndPaint (hWnd, hdc);
    return 0;
}

    这样可以避免不必要的重绘操作,从而提高绘图效率。


2.2 系统内存中的设备上下文
    MiniGUI 也提供了内存设备上下文的创建和销毁函数。利用内存设备上下文,可以在系统内存中建立一个类似显示内存的区域,然后在该区域中进行绘图操作,结束后再复制到显示内存中。这种绘图方法有许多好处,比如速度很快,减少直接操作显存造成的闪烁现象等等。不过,目前 MiniGUI 中只能建立和显示内存,也就是物理设备上下文一样的内存设备上下文。用来建立和销毁内存设备上下文的函数原型如下(include/gdi.h):

401 HDC GUIAPI CreateCompatibleDC (HDC hdc);
402 void GUIAPI DeleteCompatibleDC (HDC hdc);

2.3 屏幕设备上下文
    MiniGUI 在启动之后,就建立了一个全局的屏幕设备上下文。该 DC 是针对整个屏幕的,并且没有任何预先定义的剪切域。在某些应用程序中,可以直接使用该设备上下文进行绘图,将大大提高绘图效率。在 MiniGUI 中,屏幕设备上下文用 HDC_SCREEN 标识,不需要进行任何获取和释放操作。

2.4 映射模式
    一个设备上下文被初始化之后,其坐标系原点通常是输出矩形的左上角,而 x 轴水平向左,y 轴垂直向下,并以象素为单位。这种坐标的映射模式标识为 MM_TEXT。MiniGUI 提供了一套函数,可以改变这种映射方式,包括对默认坐标系进行偏移、缩放等操作。这些函数的原型如下(include/gdi.h):

453 int GUIAPI GetMapMode (HDC hdc);
454 void GUIAPI GetViewportExt (HDC hdc, POINT* pPt);
455 void GUIAPI GetViewportOrg (HDC hdc, POINT* pPt);
456 void GUIAPI GetWindowExt (HDC hdc, POINT* pPt);
457 void GUIAPI GetWindowOrg (HDC hdc, POINT* pPt);
458 void GUIAPI SetMapMode (HDC hdc, int mapmode);
459 void GUIAPI SetViewportExt (HDC hdc, POINT* pPt);
460 void GUIAPI SetViewportOrg (HDC hdc, POINT* pPt);
461 void GUIAPI SetWindowExt (HDC hdc, POINT* pPt);
462 void GUIAPI SetWindowOrg (HDC hdc, POINT* pPt);

    GetMapMode 函数返回当前的映射模式,若不是 MM_TEXT 模式,则返回MM_ANISOTROPIC。SetMapMode 函数设置映射模式,MiniGUI 目前只支持两种映射模式,即MM_ANISOTROPIC 和 MM_TEXT。Get 函数组用来返回映射模式信息,包括偏移量、缩放比例等等,而 Set 函数组用来设置相应的映射信息。

    通常情况下,MiniGUI 的 GDI 函数所指定的坐标参数称为"逻辑坐标",在绘制之前,首先要转化成"设备坐标"。当使用 MM_TEXT 映射模式时,逻辑坐标和设备坐标是等价的。LPtoDP 函数用来完成逻辑坐标到设备坐标的转换,DPtoLP 函数用来完成从设备坐标到逻辑坐标的转换。逻辑坐标和设备坐标的关系可从 LPtoDP 函数中看到(src/gdi/coor.c):

  61 void GUIAPI LPtoDP(HDC hdc, POINT* pPt)
  62 {
  63     PDC pdc;
  64
  65     pdc = dc_HDC2PDC(hdc);
  66
  67     if (pdc->mapmode != MM_TEXT) {
  68         pPt->x = (pPt->x - pdc->WindowOrig.x)
  69              * pdc->ViewExtent.x / pdc->WindowExtent.x
  70              + pdc->ViewOrig.x;
  71
  72         pPt->y = (pPt->y - pdc->WindowOrig.y)
  73              * pdc->ViewExtent.y / pdc->WindowExtent.y
  74              + pdc->ViewOrig.y;
  75     }
  76 }
  77

另外,LPtoSP 函数和 SPtoLP 函数完成逻辑坐标和屏幕坐标之间的转换。

3 矩形操作和区域操作

3.1 矩形操作

    在 MiniGUI 中,矩形是如下定义的(include/common.h):

120 typedef struct tagRECT
121 {
122     int left;
123     int top;
124     int right;
125     int bottom;
126 } RECT;
127 typedef RECT* PRECT;
128 typedef RECT* LPRECT;

    简而言之,矩形就是用来表示屏幕上一个矩形区域的数据结构,定义了矩形左上角的 x, y 坐标(left 和 top)以及右下角的 x, y 坐标(right 和 bottom)。需要注意的是,MiniGUI 中的矩形,其右侧的边和下面的边是不属于该矩形的。例如,要表示屏幕上的一条扫描线,应该用
RECT rc = {x, y, x + w + 1, y + 1};
表示。其中 x 是扫描线的起点,y 是扫描线的垂直位置,w 是扫描线宽度。

    MiniGUI 提供了一组函数,可对 RECT 对象进行操作:

SetRect 对 RECT 对象的各个分量进行赋值;
SetRectEmpty 将 RECT 对象设置为空。MiniGUI 中的空矩形定义为高度或宽度为零的矩形;
IsRectEmpty 判断给定 RECT 对象是否为空。
NormalizeRect 对给定矩形进行正规化处理。MiniGUI 中的矩形,应该满足(right > left 并且 bottom > top)的条件。满足这一条件的矩形又称"正规化矩形",该函数可以对任意矩形进行正规化处理。
CopyRect 复制矩形;
EqualRect 判断两个 RECT 对象是否相等,即两个 RECT 对象的各个分量相等;
IntersectRect 该函数求两个 RECT 对象之交集。若两个矩形根本不相交,则函数返回 FALSE,且结果矩形未定义;否则返回交矩形。
DoesIntersec 该函数仅仅判断两个矩形是否相交。
IsCovered 该函数判断 RECT 对象 A 是否全部覆盖 RECT 对象 B,即 RECT B 是 RECT A 的真子集。
UnionRect 该函数求两个矩形之并。如果两个矩形根本无法相并,则返回 FALSE。两个相并之后的矩形,其中所包含的任意点,应该属于两个相并矩形之一。
GetBoundRect 该函数求两个矩形的外包最小矩形。
SubstractRect 该函数从一个矩形中减去另外一个矩形。注意,两个矩形相减的结果可能生成 4 个不相交的矩形。该函数将返回结果矩形的个数以及差矩形。详细信息可参见"MiniGUI 体系结构之二――多窗口管理和控件及控件类"一文。
OffsetRect 该函数对给定的 RECT 对象进行平移处理。
InflateRect 该函数对给定的 RECT 对象进行膨胀处理。注意膨胀之后的矩形宽度和高度是给定膨胀值的两倍。
InflateRectToPt 该函数将给定的 RECT 对象膨胀到指定的点。
PtInRect 该函数判断给定的点是否位于指定的 RECT 对象中。

3.2 区域操作
    在 MiniGUI 中,区域定义为互不相交矩形的集合,在内部用链表形式表示。MiniGUI 的区域可以用来表示窗口的剪切域、无效区域、可见区域等等。在 MiniGUI 中,区域和剪切域的定义是一样的,剪切域定义如下(include/gdi.h):

  76 // Clip Rect
  77 typedef struct tagCLIPRECT
  78 {
  79     RECT rc;
  80     struct tagCLIPRECT* next;
  81 }CLIPRECT;
  82 typedef CLIPRECT* PCLIPRECT;
  83
  84 // Clip Region
  85 typedef struct tagCLIPRGN
  86 {
  87    RECT            rcBound;     // bound rect of clip region
  88    PCLIPRECT       head;        // clip rect list head
  89    PCLIPRECT       tail;        // clip rect list tail
  90    PBLOCKHEAP      heap;        // heap of clip rect
  91 } CLIPRGN;
  92 typedef CLIPRGN* PCLIPRGN;

    每个剪切域对象有一个 BLOCKHEAP 成员。该成员是剪切域分配 RECT 对象的私有堆。在使用一个剪切域对象之前,首先应该建立一个 BLOCKHEAP 对象,并对剪切域对象进行初始化。如下所示:

static BLOCKHEAP sg_MyFreeClipRectList;

...

    CLIPRGN my_region

    InitFreeClipRectList (&sg_MyFreeClipRectList, 20);
    InitClipRgn (&my_regioni, &sg_MyFreeClipRectList);

    在实际使用当中,多个剪切域可以共享同一个 BLOCKHEAP 对象。

    在初始化剪切域对象之后,可以对剪切域进行如下操作:

SetClipRgn 该函数将剪切域设置为仅包含一个矩形的剪切域;
ClipRgnCopy 该函数复制剪切域;
ClipRgnIntersect 该函数求两个剪切域的交集;
GetClipRgnBoundRect 该函数求剪切域的外包最小矩形;
IsEmptyClipRgn 该函数判断剪切域是否为空,即是否包含剪切矩形;
EmptyClipRgn 该函数释放剪切域中的剪切矩形,并清空剪切域;
AddClipRect 该函数将一个剪切矩形追加到剪切域中。注意该操作并不判断该剪切域是否和剪切矩形相交。
IntersectClipRect 该函数求剪切区域和给定矩形相交的剪切区域。
SubtractClipRect 该函数从剪切区域中减去指定的矩形。
矩形和区域的运算构成了窗口管理的主要算法,也是高级 GDI 函数的基本算法之一,在 GUI 编程中占有非常重要的地位。

4 基本图形操作
4.1 基本绘图属性
    在了解基本绘图函数之前,我们首先了解一下基本绘图属性。在 MiniGUI 的目前版本中,绘图属性比较少,大体包括线条颜色、填充颜色、文本背景模式、文本颜色、TAB 键宽度等等。表 1 给出了这些属性的操作函数。

表 1 基本绘图属性及其操作函数

绘图属性 操作函数 受影响的 GDI 函数
线条颜色 GetPenColor/SetPenColor LineTo、Circle、Rectangle
填充颜色 GetBrushColor/SetBrushColor FillBox
文本背景模式 GetBkMode/SetBkMode TextOut、DrawText
文本颜色 GetTextColor/SetTextColor 同上
TAB 键宽度 GetTabStop/SetTabStop 同上

    MiniGUI 目前版本中还定义了刷子和笔的若干函数,这些函数是为将来兼容性而定义的,目前无用。

4.2 基本绘图函数
    MiniGUI 中的基本绘图函数为点、线、圆、矩形、调色板操作等基本函数,原型定义如下(include/gdi.h,版本 1.0.06):

433 // Palette support
434 int GUIAPI GetPalette (HDC hdc, int start, int len, gal_color* cmap);
435 int GUIAPI SetPalette (HDC hdc, int start, int len, gal_color* cmap);
436 int GUIAPI SetColorfulPalette (HDC hdc);
437
438 // General drawing support
439 void GUIAPI SetPixel (HDC hdc, int x, int y, gal_pixel c);
440 void GUIAPI SetPixelRGB (HDC hdc, int x, int y, int r, int g, int b);
441 gal_pixel GUIAPI GetPixel (HDC hdc, int x, int y);
442 void GUIAPI GetPixelRGB (HDC hdc, int x, int y, int* r, int* g, int* b);
443 gal_pixel GUIAPI RGB2Pixel (HDC hdc, int r, int g, int b);
444
445 void GUIAPI LineTo (HDC hdc, int x, int y);
446 void GUIAPI MoveTo (HDC hdc, int x, int y);
447
448 void GUIAPI Circle (HDC hdc, int x, int y, int r);
449 void GUIAPI Rectangle (HDC hdc, int x0, int y0, int x1, int y1);

    这里有两个基本的概念需要明确区分,即象素值和 RGB 值。RGB 是计算机中通过三原色的不同比例表示某种颜色的方法。通常,RGB 中的红、绿、蓝可取 0 ~ 255 当中的任意值,从而可以表示 255x255x255 种不同的颜色。而在显示内存当中,要显示在屏幕上的颜色并不是用 RGB 这种方式表示的,显存当中保存的其实是所有象素的象素值。象素值的范围根据显示模式的不同而变化。在 16 色显示模式下,象素值范围为 [0, 15];而在 256 色模式下,象素值范围为 [0, 255];在 16 位色模式下,象素值范围为 [0, 2^16 - 1]。通常我们所说显示模式是多少位色,就是指象素的位数。

    在 MiniGUI 中,设置某个象素点的颜色,既可以直接使用象素值(SetPixel),也可以间接通过 RGB 值来设置(SetPixelRGB),并且通过 RGB2Pixel 函数,可以将 RGB 值转换为象素值。

    调色板是低颜色位数的模式下(比如 256 色或者更少的颜色模式),用来建立有限的象素值和 RGB 对应关系的一个线性表。在 MiniGUI 当中,可以通过 SetPalette和 GetPalette 进行调色板的操作,而SetColorfulePalette 将调色板设置为默认的调色板。一般而言,在更高的颜色位数,比如 15 位色以上,因为象素值范围能够表达的颜色已经非常丰富了,加上存储的关系,就不再使用调色板建立象素值和 RGB 的对应关系,而使用更简单的方法建立 RGB 和实际象素之间的关系,如下所示(src/gal/native/native.h):

174 /* Truecolor color conversion and extraction macros */
175 /*
176  * Conversion from RGB to gal_pixel
177  */
178 /* create 24 bit 8/8/8 format pixel (0x00RRGGBB) from RGB triplet*/
179 #define RGB2PIXEL888(r,g,b)     \
180         (((r) << 16) | ((g) << 8) | (b))
181
182 /* create 16 bit 5/6/5 format pixel from RGB triplet */
183 #define RGB2PIXEL565(r,g,b)     \
184         ((((r) & 0xf8) << 8) | (((g) & 0xfc) << 3) | (((b) & 0xf8) >> 3))
185
186 /* create 15 bit 5/5/5 format pixel from RGB triplet */
187 #define RGB2PIXEL555(r,g,b)     \
188         ((((r) & 0xf8) << 7) | (((g) & 0xf8) << 2) | (((b) & 0xf8) >> 3))
189
190 /* create 8 bit 3/3/2 format pixel from RGB triplet*/
191 #define RGB2PIXEL332(r,g,b)     \
192         (((r) & 0xe0) | (((g) & 0xe0) >> 3) | (((b) & 0xc0) >> 6))

    RGB2PIXEL888 将 [0, 255] 的 RGB 值转换为 24 位色的象素值;而 RGB2PIXEL565 转换为 16 位色的象素值;RGB2PIXEL555 和RGB2PIXEL332 分别转换为 15 位色和 8 位色。


4.3 剪切域操作函数
    在利用设备上下文进行绘图时,还可以进行剪切处理。MiniGUI 提供了如下函数完成对指定设备上下文的剪切处理(include/gdi.h):

468 // Clipping support
469 void GUIAPI ExcludeClipRect (HDC hdc, int left, int top,
470                             int right, int bottom);
471 void GUIAPI IncludeClipRect (HDC hdc, int left, int top,
472                               int right, int bottom);
473 void GUIAPI ClipRectIntersect (HDC hdc, const RECT* prc);
474 void GUIAPI SelectClipRect (HDC hdc, const RECT* prc);
475 void GUIAPI SelectClipRegion (HDC hdc, const CLIPRGN* pRgn);
476 void GUIAPI GetBoundsRect (HDC hdc, RECT* pRect);
477 BOOL GUIAPI PtVisible (HDC hdc, const POINT* pPt);
478 BOOL GUIAPI RectVisible (HDC hdc, const RECT* pRect);

    ExcludeClipRect 从设备上下文的当前可见区域中排除给定的矩形区域,设备上下文的可见区域将缩小;IncludeClipRect 向当前设备上下文的可见区域中添加一个矩形区域,设备上下文的可见区域将扩大;ClipRectIntersect 将设备上下文的可见区域设置为已有区域和给定矩形区域的交集;SelectClipRect 将设备上下文的可见区域重置为一个矩形区域;SelectClipRegion 将设备上下文的可见区域设置为一个指定的区域;GetBoundsRect 获取当前可见区域的外包最小矩形;PtVisible 和 RectVisible 用来判断给定的点或者矩形是否可见,即是否全部或部分落在可见区域当中。


5 位图操作函数
    在 MiniGUI 的 GDI 函数中,位图操作函数占有非常重要的地位。实际上,许多高级绘图操作函数均建立在位图操作函数之上,比如文本输出函数。MiniGUI 的主要位图操作函数如下所示(include/gdi.h):

495 void GUIAPI FillBox (HDC hdc, int x, int y, int w, int h);
496 void GUIAPI FillBoxWithBitmap (HDC hdc, int x, int y, int w, int h,
497                               PBITMAP pBitmap);
498 void GUIAPI FillBoxWithBitmapPart (HDC hdc, int x, int y, int w, int h,
499                               int bw, int bh, PBITMAP pBitmap, int xo, int yo);
500
501 void GUIAPI BitBlt (HDC hsdc, int sx, int sy, int sw, int sh,
502                    HDC hddc, int dx, int dy, DWORD dwRop);
503 void GUIAPI StretchBlt (HDC hsdc, int sx, int sy, int sw, int sh,
504                        HDC hddc, int dx, int dy, int dw, int dh, DWORD dwRop);

    FillBox 用当前填充色填充矩形框;FillBoxWithBitmap 用设备相关位图对象填充矩形框,可以用来扩大或者缩小位图;FillBoxWithBitmapPart 用设备相关位图对象的部分填充矩形框,也可以扩大或缩小位图。BitBlt 函数用来实现两个不同设备上下文之间的显示内存复制。StretchBlt 则在 BitBlt 的基础上进行缩放操作。

    通过 MiniGUI 的 LoadBitmap 函数,可以将某种位图文件装载为 MiniGUI 设备相关的位图对象,即 BITMAP 对象。设备相关的位图指的是,位图当中包含的是与指定设备上下文的显示模式相匹配的象素值,而不是设备无关的位图信息。MiniGUI 目前可以用来装载BMP 文件、JPG 文件、GIF 文件以及 PCX、TGA 等格式的位图文件,而 LoadMyBitmap 函数则用来将位图文件装载成设备无关的位图对象。在 MiniGUI 中,设备相关的位图对象和设备无关的位图对象分别用 BITMAP 和 MYBITMAP 两种数据结构表示。相关函数的原型如下(include/gdi.h):

666 int GUIAPI LoadMyBitmap (HDC hdc, PMYBITMAP pMyBitmap, RGB* pal, const char* spFileName);
667 int GUIAPI LoadBitmap (HDC hdc, PBITMAP pBitmap, const char* spFileName);
668 #ifdef _SAVE_BITMAP
669 int GUIAPI SaveBitmap (HDC hdc, PBITMAP pBitmap, const char* spFileName);
670 #endif
671 void GUIAPI UnloadBitmap (PBITMAP pBitmap);
672
673 int GUIAPI ExpandMyBitmap (HDC hdc, const MYBITMAP* pMyBitmap, const RGB* pal, PBITMAP pBitmap);
674
675 void GUIAPI ExpandMonoBitmap (HDC hdc, int w, int h, const BYTE* bits, int bits_flow, int pitch,
676                              BYTE* bitmap, int bg, int fg);
677 void GUIAPI Expand16CBitmap (HDC hdc, int w, int h, const BYTE* bits, int bits_flow, int pitch,
678                             BYTE* bitmap, const RGB* pal);
679 void GUIAPI Expand256CBitmap (HDC hdc, int w, int h, const BYTE* bits, int bits_flow, int pitch,
680                              BYTE* bitmap, const RGB* pal);
681 void GUIAPI CompileRGBBitmap (HDC hdc, int w, int h, const BYTE* bits, int bits_flow, int pitch,
682                              BYTE* bitmap, int rgb_order);
683
684 void GUIAPI ReplaceBitmapColor (HDC hdc, PBITMAP pBitmap, int iOColor, int iNColor);

    上面的 Expand 函数组,用来将设备无关的位图转化为与指定设备上下文相关的位图对象。

    有关位图操作的详细使用方法,可见 mglite-exec 包中的 bitmaptest 示例程序。

6 逻辑字体和文本输出函数
    MiniGUI 的逻辑字体功能强大,它包括了字符集、字体类型、风格、样式等等丰富的信息,不仅仅可以用来输出文本,而且可以用来分析多语种文本的结构。这在许多文本排版应用中非常有用。在使用 MiniGUI 的逻辑字体之前,首先要创建逻辑字体,并且将其选择到要使用这种逻辑字体进行文本输出的设备上下文当中。每个设备上下文的默认逻辑字体是系统字体,即用来显示菜单、标题的逻辑字体。你可以调用 CreateLogFont 和 CreateLogFontIndirect 两个函数来建立逻辑字体,并利用 SelectFont 函数将逻辑字体选择到指定的设备上下文中,在使用结束之后,用 DestroyLogFont 函数销毁逻辑字体。注意你不能销毁正被选中的逻辑字体。这几个函数的原型如下(include/gdi.h):

555 PLOGFONT GUIAPI CreateLogFont (const char* type, const char* family,
556         const char* charset, char weight, char slant, char set_width,
557         char spacing, char underline, char struckout,
558         int size, int rotation);
559 PLOGFONT GUIAPI CreateLogFontIndirect (LOGFONT* logfont);
560 void GUIAPI DestroyLogFont (PLOGFONT log_font);
561
562 void GUIAPI GetLogFontInfo (HDC hdc, LOGFONT* log_font);
563
564 #define SYSLOGFONT_DEFAULT          0
565 PLOGFONT GUIAPI GetSystemFont (int font_id);
566
567 PLOGFONT GUIAPI GetCurFont (HDC hdc);
568 PLOGFONT GUIAPI SelectFont (HDC hdc, PLOGFONT log_font);

    GetSystemFont 函数返回默认的系统逻辑字体,GetCurFont 函数返回当前选中的逻辑字体。注意不要删除系统逻辑字体。下面的程序段建立了多个逻辑字体:

   static LOGFONT  *logfont, *logfontgb12, *logfontbig24;

            logfont = CreateLogFont (NULL, "SansSerif", "ISO8859-1",
                        FONT_WEIGHT_REGULAR, FONT_SLANT_ITALIC, FONT_SETWIDTH_NORMAL,
                        FONT_SPACING_CHARCELL, FONT_UNDERLINE_NONE, FONT_STRUCKOUT_LINE,
                        16, 0);
            logfontgb12 = CreateLogFont (NULL, "song", "GB2312",
                        FONT_WEIGHT_REGULAR, FONT_SLANT_ROMAN, FONT_SETWIDTH_NORMAL,
                        FONT_SPACING_CHARCELL, FONT_UNDERLINE_LINE, FONT_STRUCKOUT_LINE,
                        12, 0);
            logfontbig24 = CreateLogFont (NULL, "ming", "BIG5",
                        FONT_WEIGHT_REGULAR, FONT_SLANT_ROMAN, FONT_SETWIDTH_NORMAL,
                        FONT_SPACING_CHARCELL, FONT_UNDERLINE_LINE, FONT_STRUCKOUT_NONE,
                        24, 0);

    其中,第一个字体,即 logfont 是属于字符集 ISO8859-1 的字体,并且选用 SansSerif 体,大小为 16 象素高;logfontgb12 是属于字符集 GB2312 的字体,并选用 song 体(宋体),大小为 12 象素高;logfontbig24 是属于字符集 BIG5 的字体,并选用 ming 体(即明体)。

    在建立了逻辑字体之后,应用程序可以利用逻辑字体进行多语种混和文本的分析。这里的多语种混和文本是指,两个不相交字符集的文本组成的字符串,比如 GB2312 和 ISO8859-1,或者 BIG5 和 ISO8859-2,通常是多字符集和单字符集之间的混和。利用下面的函数,可以实现多语种混和文本的文本组成分析(include/gdi.h):

570 // Text parse support
571 int GUIAPI GetTextMCharInfo (PLOGFONT log_font, const char* mstr, int len,
572                 int* pos_chars);
573 int GUIAPI GetTextWordInfo (PLOGFONT log_font, const char* mstr, int len,
574                 int* pos_words, WORDINFO* info_words);
575 int GUIAPI GetFirstMCharLen (PLOGFONT log_font, const char* mstr, int len);
576 int GUIAPI GetFirstWord (PLOGFONT log_font, const char* mstr, int len,
577                             WORDINFO* word_info);

    GetTextMCharInfo 函数返回多语种混和文本中每个字符的字节位置。比如对"ABC汉语"字符串,该函数将在 pos_chars 中返回{0, 1, 2, 3, 5} 5 个值。GetTextWordInfo 函数则将分析多语种混和文本中每个单词的位置。对单字节字符集文本,单词以空格、TAB 键为分界,对多字节字符集文本,单词以单个字符为界。GetFirstMCharLen 函数返回第一个混和文本字符的字节长度。GetFirstWord 函数返回第一个混和文本单词的单词信息。

    以下函数可以用来计算逻辑字体的输出长度和高度信息(include/gdi.h):

580 int GUIAPI GetTextExtentPoint (HDC hdc, const char* text, int len, int max_extent,
581                 int* fit_chars, int* pos_chars, int* dx_chars, SIZE* size);
582
583 // Text output support
584 int GUIAPI GetFontHeight (HDC hdc);
585 int GUIAPI GetMaxFontWidth (HDC hdc);
586 void GUIAPI GetTextExtent (HDC hdc, const char* spText, int len, SIZE* pSize);
587 void GUIAPI GetTabbedTextExtent (HDC hdc, const char* spText, int len, SIZE* pSize);

    GetTextExtentPoint 函数计算在给定的输出宽度内输出多字节文本时(即输出的字符限制在一定的宽度当中),可输出的最大字符个数、每个字符所在的字节位置、每个字符的输出位置,以及实际的输出高度和宽度。GetFontHeight 和 GetMaxFontWidth 则返回逻辑字体的高度和最大字符宽度。GetTextExtent 计算文本的输出高度和宽度。GetTabbedTextExtent 函数返回格式化字符串的输出高度和宽度。

    以下函数用来输出文本(include/gdi.h):

596 int GUIAPI TextOutLen (HDC hdc, int x, int y, const char* spText, int len);
597 int GUIAPI TabbedTextOutLen (HDC hdc, int x, int y, const char* spText, int len);
598 int GUIAPI TabbedTextOutEx (HDC hdc, int x, int y, const char* spText, int nCount,
599             int nTabPositions, int *pTabPositions, int nTabOrigin);
600 void GUIAPI GetLastTextOutPos (HDC hdc, POINT* pt);
601
602 // Compatiblity definitions
603 #define TextOut(hdc, x, y, text)    TextOutLen (hdc, x, y, text, -1)
604 #define TabbedTextOut(hdc, x, y, text)  TabbedTextOutLen (hdc, x, y, text, -1)

...

621 int GUIAPI DrawTextEx (HDC hdc, const char* pText, int nCount,
622                 RECT* pRect, int nIndent, UINT nFormat);

    TextOutLen 函数用来在给定位置输出指定长度的字符串,若长度为 -1,则字符串必须是以 '\0' 结尾的。TabbedTextOutLen 函数用来输出格式化字符串。TabbedTextOutEx 函数用来输出格式化字符串,但可以指定字符串中每个 TAB 键的位置。DrawText 是功能最复杂的输出函数,可以以不同的对齐方式在指定矩形内部输出文本。下面的程序段,就根据字符串所描述的那样,调用 DrawText 函数进行对齐文本输出:

void OnModeDrawText (HDC hdc)
{
    RECT rc1, rc2, rc3, rc4;
    const char* szBuff1 = "This is a good day. \n"
            "这是利用 DrawText 绘制的文本, 使用字体 GB2312 Song 12. "
            "文本垂直靠上, 水平居中";
    const char* szBuff2 = "This is a good day. \n"
            "这是利用 DrawText 绘制的文本, 使用字体 GB2312 Song 16. "
            "文本垂直靠上, 水平靠右";
    const char* szBuff3 = "单行文本垂直居中, 水平居中";
    const char* szBuff4 =
            "这是利用 DrawTextEx 绘制的文本, 使用字体 GB2312 Song 16. "
            "首行缩进值为 32. 文本垂直靠上, 水平靠左";

    rc1.left = 1; rc1.top  = 1; rc1.right = 401; rc1.bottom = 101;
    rc2.left = 0; rc2.top  = 110; rc2.right = 401; rc2.bottom = 351;
    rc3.left = 0; rc3.top  = 361; rc3.right = 401; rc3.bottom = 451;
    rc4.left = 0; rc4.top  = 461; rc4.right = 401; rc4.bottom = 551;

    SetBkColor (hdc, COLOR_lightwhite);

    Rectangle (hdc, rc1.left, rc1.top, rc1.right, rc1.bottom);
    Rectangle (hdc, rc2.left, rc2.top, rc2.right, rc2.bottom);
    Rectangle (hdc, rc3.left, rc3.top, rc3.right, rc3.bottom);
    Rectangle (hdc, rc4.left, rc4.top, rc4.right, rc4.bottom);

    InflateRect (&rc1, -1, -1);
    InflateRect (&rc2, -1, -1);
    InflateRect (&rc3, -1, -1);
    InflateRect (&rc4, -1, -1);

    SelectFont (hdc, logfontgb12);
    DrawText (hdc, szBuff1, -1, &rc1, DT_NOCLIP | DT_CENTER | DT_WORDBREAK);

    SelectFont (hdc, logfontgb16);
    DrawText (hdc, szBuff2, -1, &rc2, DT_NOCLIP | DT_RIGHT | DT_WORDBREAK);

    SelectFont (hdc, logfontgb24);
    DrawText (hdc, szBuff3, -1, &rc3, DT_NOCLIP | DT_SINGLELINE | DT_CENTER | DT_VCENTER);

    SelectFont (hdc, logfontgb16);
    DrawTextEx (hdc, szBuff4, -1, &rc4, 32, DT_NOCLIP | DT_LEFT | DT_WORDBREAK);
}

    有关逻辑字体和文本输出的函数详细使用方法,可见 mglite-exec 包中的 fontest 示例程序。

7 小结
    本文讲述了 MiniGUI 中接口最多也最复杂的 GDI 函数及其使用方法。其中包括:设备上下文的概念、获取和释放;矩形操作和区域操作;基本绘图函数;位图操作函数;逻辑字体操作函数等等。目前版本的 GDI 接口还有许多功能上的缺陷,我们将在下一个版本开发中着重进行改善。关于 MiniGUI 下一版本的开发计划,请参见本文附录。

附录:MiniGUI 的最新开发计划
    MiniGUI 发展到今天,得到了许多用户的认可,使用它的人也越来越多了。目前,用户已经从国内发展到了国外。这说明 MiniGUI 当中的许多设计思想得到了认可,也大大激励了我们的开发热情。

    作为一个面向实时嵌入式系统的 GUI,MiniGUI 的 1.0.xx 版本基本能够满足许多嵌入式系统的应用需求。但这还远远不够,我们仍然需要进一步的开发,以便让 MiniGUI 在嵌入式 GUI 系统中达到领先地位。

    MiniGUI 发展到今天,得到了许多用户的认可,使用它的人也越来越多了。目前,用户已经从国内发展到了国外。这说明 MiniGUI 当中的许多设计思想得到了认可,也大大激励了我们的开发热情。

    作为一个面向实时嵌入式系统的 GUI,MiniGUI 的 1.0.xx 版本基本能够满足许多嵌入式系统的应用需求。但这还远远不够,我们仍然需要进一步的开发,以便让 MiniGUI 在嵌入式 GUI 系统中达到领先地位。

    我们已经开始了 MiniGUI 新版本开发(即 1.1.xx),对这个版本,有如下新的设想:

    MiniGUI-Lite 的全局鼠标支持。目前的 MiniGUI-Lite 版本,鼠标的位置刷新是由鼠标所在客户或者服务器管理的。新版本中,将考虑由服务器统一管理。这个工作目前已经基本完成。
    在 MiniGUI-Lite 中添加层(Layer)的概念和处理。在一次 MiniGUI-Lite 会话中,可以建立多个层。每个层中可以包含能够同时向屏幕输出的多个客户,而每一时刻,能够在屏幕上显示的层只有一个。对层而言,我们可以进行层的激活处理。激活的层,将显示在屏幕上,而其他层的绘图将被屏蔽。对层中客户的绘图屏蔽算法,将考虑使用不同于当前 MiniGUI-Lite 通过信号和信号量结合的方法,因为这种方法在多线程应用中,可能出现问题。
    层中客户可以互相剪切。后建立的客户,将剪切先建立的客户矩形。为此,要为每个层建立一个共享内存的 IPC 对象,客户通过该对象访问当前层客户之间的重叠和覆盖情况,而且要建立一个面向层的信号量和 age 值,用来协调客户剪切矩形的变化。层的客户剪切矩形的变化,将影响各个客户所建立窗口的全局剪切区域,从而影响 DC 的可见区域。
    一个层中客户之间形成的 Z 序是固定的。不过,如果按照 3 所描述的方法,其实 Z 序也是可以变化的。考虑到性能因素,客户在层中所占的显示矩形不能变化,也就是说,既不能改变大小,也不能移动。但能够改变 Z 序,即改变客户之间的互相层叠关系。
BTW:为什么要如此考虑?
    通过上面的方法,可以将一组具有共同目标的客户放置在同一个层上。比如,层中可以有一个 vcongui 程序,用它可以调试其他的 MiniGUI 程序。再比如,在 VOD 等程序中,实时播放 VCD 的客户就可以嵌入到主控界面当中。而服务器将具有较少的 GUI 能力,仅仅提供一个任务栏,用来激活某个层,或者改变客户在一个层中的 Z 序。

    当然,这样安排对某些小屏幕的嵌入式应用来讲,比如 PDA,并不是非常适合,但对 STB、或者其他具有大屏幕的实时嵌入式系统来讲,将具有非常高的应用价值。

    底层图形引擎将进行非常重大的修改,这将影响到 MiniGUI-Threads 和 MiniGUI-Lite 两个版本。目前的 MiniGUI 图形引擎,因为受到历史原因的影响,有许多弊端。在新的版本中,我们将考虑类似 SDL 那样的设计方法,将底层图形设备抽象为一个内存对象,并考虑加速功能的实现。同时,我们还要实现许多尚未实现的图形功能,包括光栅操作、Alpha 混和、多边形支持、椭圆和弧线支持等等。
BTW:当前 GAL 的设计弊端
    当前 GAL 的设计弊端主要是抽象层次太高,而且并没有在底层实现剪切域的直接支持。这是要在新版本中着重考虑改进的。新的剪切域算法,将考虑生成 x-y-banned 的剪切域,以便底层绘图函数能够直接利用剪切域进行设计绘图算法。

    将考虑在 MiniGUI-Lite 版本中实现对矢量字体的支持,同时增加 Cache 处理能力,以便提高矢量字体的渲染效率。
    对窗口管理,在这次开发中将不作大的修改,主要将进行一些代码的清理工作。

    以上是我们对新版本的一些想法,希望大家能够讨论,并请多提建议和意见。我们将考虑首先实现层,然后实现图形引擎的改进,最后实现矢量字体在 MiniGUI-Lite 当中的支持及优化。如果您对 MiniGUI 新版本的开发有兴趣,可以加入我们的邮件列表,详细信息请参见http://www.minigui.org/ctalk.shtml。

[目录]


MiniGUI 1.1.0引入的新GDI功能和函数(1)

1 引言
    在本系列开发指南(四)中,我们详细讲解了 MiniGUI 的 GDI 函数及其使用。我们也曾提到,MiniGUI 现有的 GDI 函数和功能,尚不能对机顶盒、瘦客户机等高端嵌入式系统提供良好支持。因此,我们在 MiniGUI 1.1.0 版本的开发中,重点对 GAL 和 GDI 进行了大规模的改良,几乎重新编写了所有代码。这些新的接口和功能,首先出现在最近发布的 MiniGUI 1.1.0Pre4 版本中,为了帮助开发人员正确理解和使用这些功能,特撰文说明新 GAL 和新 GDI 的接口和功能。

2 新 GAL 和新 GDI 接口的设计目标
    首先,MiniGUI 旧的 GDI 接口非常简单,其功能主要集中在位图和块操作上,例如 FillBox 函数、FillBoxWithBitmap 等等,而缺少对其他高级图形操作的支持,比如椭圆、圆弧、样条曲线等等。其次,旧的 GDI 接口还缺少基本的光栅操作功能。这里的光栅操作,指欲设定象素如何与屏幕上已有的象素进行运算。最基本的光栅操作功能是二进制的位操作,包括与、或、异或以及直接设置等等;高级的光栅操作包括透明处理和 Alpha 混和。这些 GDI 功能的缺乏,使得 MiniGUI 在机顶盒、瘦客户等系统中应用时,显得"力不从心"。再次,旧的 GDI 接口基本上没有考虑到任何硬件加速功能。我们大家都知道,显示卡自身提供的硬件加速功能,能够大大提高图形程序的运行速度,使得画面流畅而自然,如果 GUI 系统不能充分利用这些硬件特性的话,则图形处理能力将大打折扣。最后,旧的 GAL 设计存在抽象层次太高的问题,导致了 GAL 引擎臃肿,且重复代码很多,也不便于进行代码上的优化。

    综上所述,我们参照著名的跨平台游戏和多媒体函数库 SDL(Simple DirectMedia Layer)对 GAL 引擎结构进行了重新设计,并且重新实现了所有的 GDI 函数,使得新的 GDI 接口具备如下特性:

    ·能够充分利用硬件特性,包括显示内存和硬件加速能力。
    ·支持高级图形操作,包括基本光栅操作、透明处理和 Alpha 混和。
    ·增强了剪切区域处理能力,有助于优化图形输出函数。
    ·增强了原有的 BITMAP 接口,使之支持透明和 Alpha 通道。
    ·充分利用嵌入式汇编代码进行代码优化。

下面将重点讲述新的 GAL 功能和新的 GDI 接口。

3 新 GAL 功能特性
    新的 GAL 结构来自著名的跨平台游戏和多媒体库 SDL(Simple DirectMedia Layer)。目前提供了对 Linux FrameBuffer 的支持,计划在将来提供对 X、SVGALib 和 VGL(FreeBSD)等等图形库的支持。

3.1 GAL 和 GDI 的关系
    大家都知道,MiniGUI 的 GAL 是一个图形抽象层,提供给上层 GDI 函数一些基础的功能和设施。在先前的设计中,GAL 可以看成是 GDI 图形驱动程序,许多图形操作函数,比如点、线、矩形填充、位图操作等等,均通过 GAL 的相应函数完成。这种设计的最大问题是无法对 GDI 进行扩展。比如要增加椭圆绘制函数,就需要在每个引擎当中实现椭圆的绘制函数。并且 GDI 管理的是剪切域,而 GAL 引擎却基于剪切矩形进行操作。这种方法也导致了 GDI 函数无法进行绘制优化。因此,在新的 GAL 和 GDI 接口设计中,我们将 GAL 的接口进行了限制,而将原有许多由 GAL 引擎完成的图形输出函数,提高到上层 GDI 函数中完成。GAL 和 GDI 的新的功能划分如下:

    ·GAL 负责对显示设备进行初始化,并管理显示内存的使用;
    ·GAL 负责为上层 GDI 提供映射到进程地址空间的线性显示内存,以及诸如调色板等其他相关信息;
    ·GAL 负责实现快速的位块操作,包括矩形填充和 Blitting 操作等,并且在可能的情况下,充分利用硬件加速功能;
    ·GDI 函数实现高级图形功能,包括点、线、圆、椭圆、圆弧、样条曲线,以及更加高级的逻辑画笔和逻辑画刷,必要时调用 GAL 接口完成加速功能;

    尽管某些显示卡也提供有对上述高级绘图功能的硬件支持,但考虑到其他因素,这些硬件加速功能不由 GAL 接口提供;而统统通过软件实现。

    这样,GAL 主要实现的绘图功能限制在位块操作上,比如矩形填充和 Blitting 操作;而其他的高级图形功能,则全部由 GDI 函数实现。

3.2 显示内存的有效利用
    新的 GAL 接口能够有效利用显示卡上的显示内存,并充分利用硬件加速功能。我们知道,现在显示卡一般具有 4M 以上的显示内存,而一般的显示模式下,不会占用所有的显示内存。比如在显示模式为 1204x768x32bpp 时,一屏象素所占用的内存为 3M,还有 1M 的内存可供应用程序使用。因此,新的 GAL 引擎能够管理这部分未被使用的显示内存,并分配给应用程序使用。这样,一方面可以节省系统内存的使用,另一方面,可以充分利用显示卡提供的加速功能,在显示内存的两个不同内存区域之间进行快速的位块操作,也就是常说的 Blitting。

3.3 Blitting 操作
    在上层 GDI 接口在建立内存 DC 设备时,将首先在显示内存上分配内存,如果失败,才会考虑使用系统内存。这样,如果 GAL 引擎提供了硬件加速功能,两个不同 DC 设备之间的 Blitting 操作(即 GDI 函数 BitBlt),将以最快的速度运行。更进一步,如果硬件支持透明或 Alpha 混和功能,则透明的或者 Alpha 混和的 Blitting 操作也将以最快的速度运行。新的 GAL 接口能够根据底层引擎的加速能力自动利用这些硬件加速功能。目前支持的硬件加速能力主要有:矩形填充,普通的 Blitting 操作,透明、Alpha 混和的 Blitting 操作等。当然,如果硬件不支持这些加速功能,新的 GAL 接口也能够通过软件实现这些功能。目前通过 GAL 的 FrameBuffer 引擎提供上述硬件加速功能的显卡有:Matrox、3dfx 等。

    在通过软件实现透明或混和的 DC 间 Blitting 操作时,新的 GAL 接口利用了两种有效的优化措施:

    在 i386 平台上,充分利用嵌入式汇编代码进行优化处理;比如在处理 32 位色模式下的普通 Blitting 操作时,在利用普通的 C 库函数,即 memcpy 进行位块复制时,由于 memcpy 函数是以字节为单位进行复制的,从而无法利用 32 位 CPU 对 32 位字的处理能力,为此,可以使用嵌入式汇编,并以 32 位字为单位进行复制,这将大大提高 Bliting 操作的处理速度。
    对源 DC 进行 RLE(Run Length Encoding)编码,从而对象素的处理数量最小化。RLE 可以看成是一种图象压缩算法,Windows BMP 文件就利用了这种算法。RLE 是按水平扫描线进行压缩编码处理的。在一条扫描线上,如果有大量相同的象素,则不会保存这些象素点,而是首先保存具有相同象素点的数目,然后保存这些象素点的值。这样,在进行透明或者混和的 Blitting 操作时,可以大大降低逐点运算带来的速度损失。但是,如果在最坏的情况下,比如所有水平扫描线上的象素点都具有和相邻点不同的象素值,则 RLE 编码反而会增加象素的存储空间(最坏的情况是原有空间的两倍),同时也会降低 Blitting 操作的速度。因此是否使用 RLE 编码,要根据情况而定。新的 GDI 接口在指定源 DC 的透明和 Alpha 通道值时,可以指定是否使用 RLE 编码。

3.4 有效分辨率
    新的 GAL 引擎可以设定一个不同于实际显示分辨率的有效分辨率。比如实际的显示分辨率是 1024x768,则可以在 /etc/MiniGUI.cfg 文件中指定比实际分辨率低的有效分辨率。这种特性有利于在 PC 上调试需要运行在较小分辨率系统上的应用程序。比如:

[system]
gal_engine=native
ial_engine=native

[native]
defaultmode=320x240x16

    其中在 defaultmode 当中指定了有效分辨率为 320x240,16 则表示颜色深度,即 16 位色,或者称为每象素的二进制位数。需要注意的是,对 VESA FramBuffer 设备,必须指定和当前颜色深度一致的颜色深度值。对其他的 FrameBuffer 设备,如果能够支持多种显示模式,则会根据 defaultmode 指定的模式设置当前分辨率。

3.5 新 GAL 的限制
    需要注意的是,新的 GAL 结构只打算支持线性显示内存,并且只支持 8 位色以上的显示模式。如果要支持低于 8 位色的显示模式,则可以选择使用老的 GAL 和 GDI 接口。在配置 MiniGUI 的时候,你可以指定是否使用老的 GAL 和 GDI 接口。默认情况下的配置是使用新的 GAL 和 GDI 接口,需要使用老的 GAL 和 GDI 接口时,应进行如下的配置:

./configure --disable-newgal

    另外,新的 GAL 接口支持 Gamma 校正和 YUV Overlay,但目前尚未在 GDI 接口中体现这些功能。在新的版本中,会逐步添加相应的 GDI 接口。

4 新的 GDI 接口

4.1 新的区域算法
    新的 GDI 采用了新的区域算法,即在 X Window 和其他 GUI 系统当中广泛使用的区域算法。这种区域称作"x-y-banned"区域,并且具有如下特点:

区域由互不相交的非空矩形组成;
    区域又可以划分为若干互不相交的水平条带,每个水平条带中的矩形是等高,而且是上对齐的;或者说,这些矩形具有相同的高度,而且所有矩形的左上角 y 坐标相等。
区域中矩形的排列,首先是在 x 方向(在一个条带中)从左到右排列,然后按照 y 坐标从上到下排列。

    在 GDI 函数进行绘图输出时,可以利用 x-y-banned 区域的特殊性质进行绘图的优化。在将来版本中添加的绘图函数,将充分利用这一特性进行绘图输出上的优化。

    新的 GDI 增加了如下接口,可用于剪切区域的运算(include/gdi.h):

BOOL GUIAPI PtInRegion (PCLIPRGN region, int x, int y);
BOOL GUIAPI RectInRegion (PCLIPRGN region, const RECT* rect);

BOOL GUIAPI IntersectRegion (CLIPRGN *dst, const CLIPRGN *src1, const CLIPRGN *src2);
BOOL GUIAPI UnionRegion (PCLIPRGN dst, const CLIPRGN* src1, const CLIPRGN* src2);
BOOL GUIAPI SubtractRegion (CLIPRGN* rgnD, const CLIPRGN* rgnM, const CLIPRGN* rgnS);
BOOL GUIAPI XorRegion (CLIPRGN *dst, const CLIPRGN *src1, const CLIPRGN *src2);

PtInRegion 函数可用来检查给定点是否位于给定的区域中。
RectInRegion 函数可用来检查给定矩形是否和给定区域相交。
IntersectRegion 函数对两个给定区域进行求交运算。
UnionRegion 函数可合并两个不同的区域,合并后的区域仍然是 x-y-banned 的区域。
SubstractRegion 函数从一个区域中减去另外一个区域。
XorRegion 函数对两个区域进行异或运算,其结果相当于 src1 减 src2 的结果 A 与 src2 减 src1 的结果 B 之间的交。
在 MiniGUI 1.1.0 版本正式发布时,我们将添加从多边形、椭圆或圆弧等封闭曲线中生成剪切域的 GDI 函数。这样,就可以实现将 GDI 输出限制在特殊封闭曲线的效果。

4.2 光栅操作
    光栅操作是指在进行绘图输出时,如何将要输出的象素点和屏幕上已有的象素点进行运算。最典型的运算是下面要讲到的 Alpha 混和。这里的光栅操作特指二进制的位操作,包括与、或、异或和直接的设置(覆盖)等等。应用程序可以利用 SetRasterOperation/GetRasterOperation 函数设置或者获取当前的光栅操作。这两个函数的原型如下(include/gdi.h):

#define ROP_SET         0
#define ROP_AND         1
#define ROP_OR          2
#define ROP_XOR         3

int GUIAPI GetRasterOperation (HDC hdc);
int GUIAPI SetRasterOperation (HDC hdc, int rop);

    在设置了新的光栅操作之后,其后的一般图形输出将受到设定的光栅操作的影响,这些图形输出包括:SetPixel、LineTo、Circle、Rectangle、FillRect 和 FillCircle 等等。需要注意的是,新的 GDI 函数引入了一个新的矩形填充函数――FillRect。如上所述,FillRect 函数是受当前光栅操作影响的,而原先的 FillBox 函数则不受当前的光栅操作影响。这是因为 FillBox 函数会利用硬件加速功能实现矩形填充,并且该函数的填充速度要比 FillRect 函数快。

4.3 内存 DC 和 BitBlt
    新的 GDI 函数增强了内存 DC 操作函数。GDI 函数在建立内存 DC 时,将调用 GAL 的相应接口。如前所述,GAL 将尽量把内存 DC 建立在显示卡的显示内存当中。这样,可以充分利用显示卡的硬件加速功能,实现显示内存中两个不同区域之间位块的快速移动、复制等等,包括透明处理和 Alpha 混和。应用程序可以建立一个具有逐点 Alpha 特性的内存 DC(每个点具有不同的 Alpha 值),也可以通过 SetMemDCAlpha 设置内存 DC 所有象素的 Alpha 值(或者称为"Alpha 通道"),然后利用 BitBlt 和 StretchBlt 函数实现 DC 之间的位块传送。应用程序还可以通过 SetMemDCColorKey 函数设置源 DC 的透明色,从而在进行 BitBlt 时跳过这些透明色。

有关内存 DC 的 GDI 函数有(include/gdi.h):

#define MEMDC_FLAG_NONE         0x00000000          /* None. */
#define MEMDC_FLAG_SWSURFACE    0x00000000          /* DC is in system memory */
#define MEMDC_FLAG_HWSURFACE    0x00000001          /* DC is in video memory */
#define MEMDC_FLAG_SRCCOLORKEY  0x00001000          /* Blit uses a source color key */
#define MEMDC_FLAG_SRCALPHA     0x00010000          /* Blit uses source alpha blending */
#define MEMDC_FLAG_RLEACCEL     0x00004000          /* Surface is RLE encoded */

HDC GUIAPI CreateCompatibleDC (HDC hdc);
HDC GUIAPI CreateMemDC (int width, int height, int depth, DWORD flags,
                Uint32 Rmask, Uint32 Gmask, Uint32 Bmask, Uint32 Amask);
BOOL GUIAPI ConvertMemDC (HDC mem_dc, HDC ref_dc, DWORD flags);
BOOL GUIAPI SetMemDCAlpha (HDC mem_dc, DWORD flags, Uint8 alpha);
BOOL GUIAPI SetMemDCColorKey (HDC mem_dc, DWORD flags, Uint32 color_key);
void GUIAPI DeleteMemDC (HDC mem_dc);

    CreateCompatibleDC 函数创建一个和给定 DC 兼容的内存 DC。兼容的含义是指,新创建的内存 DC 的象素格式、宽度和高度与给定 DC 是相同的。利用这种方式建立的内存 DC 可以快速 Blit 到与之兼容的 DC 上。

    这里需要对象素格式做进一步解释。象素格式包含了颜色深度(即每象素点的二进制位数)、调色板或者象素点中 RGBA(红、绿、蓝、Alpha)四个分量的组成方式。其中的 Alpha 分量,可以理解为一个象素点的透明度,0 表示完全透明,255 表示完全不透明。在 MiniGUI 中,如果颜色深度低于 8,则 GAL 会默认创建一个调色板,并且可以调用 SetPalette 函数修改调色板。如果颜色深度高于 8,则通过四个变量分别指定象素点中 RGBA 分量所占的位。如果是建立兼容 DC,则兼容内存 DC 和给定 DC 具有一样的颜色深度,同时具有一样的调色板或者一样的 RGBA 分量组成方式。

    如果调用 CreateMemDC 函数,则可以指定新建内存 DC 的高度、宽度、颜色深度,以及必要的 RGBA 组成方式。在 MiniGUI 中,是通过各自在象素点中所占用的位掩码来表示 RGBA 四个分量的组成方式的。比如,如果要创建一个包含逐点 Alpha 信息的16 位内存 DC,则可以用每分量四个二进制位的方式分配 16 位的象素值,这样,RGBA 四个分量的掩码分别为:0x0000F000, 0x00000F00, 0x000000F0, 0x0000000F。

    ConvertMemDC 函数用来将一个任意的内存 DC 对象,根据给定的参考 DC 的象素格式进行转换,使得结果 DC 具有和参考 DC 一样的象素格式。这样,转换后的 DC 就能够快速 Blit 到与之兼容的 DC 上。

    SetMemDCAlpha 函数用来设定或者取消整个内存 DC 对象的 Alpha 通道值。我们还可以通过 MEMDC_FLAG_RLEACCEL 标志指定内存 DC 采用或者取消 RLE 编码方式。Alpha 通道值将作用在 DC 的所有象素点上。

    SetMemDCColorKey 函数用来设定或者取消整个内存 DC 对象的 ColorKey,即透明象素值。我们还可以通过 MEMDC_FLAG_RLEACCEL 标志指定内存 DC 采用或者取消 RLE 编码方式。

    内存 DC 和其他 DC 一样,也可以调用 GDI 的绘图函数向内存 DC 中进行任意的绘图输出,然后再 BitBlt 到其他 DC 中。下面的程序段演示了如何使用内存 DC 向窗口 DC 进行透明和 Alpha 混和的 Blitting 操作:

  /* 逐点 Alpha 操作 */
    mem_dc = CreateMemDC (400, 100, 16, MEMDC_FLAG_HWSURFACE | MEMDC_FLAG_SRCALPHA,
                    0x0000F000, 0x00000F00, 0x000000F0, 0x0000000F);

    /* 设置一个不透明的刷子并填充矩形 */
    SetBrushColor (mem_dc, RGBA2Pixel (mem_dc, 0xFF, 0xFF, 0x00, 0xFF));
    FillBox (mem_dc, 0, 0, 200, 50);

    /* 设置一个 25%  透明的刷子并填充矩形 */
    SetBrushColor (mem_dc, RGBA2Pixel (mem_dc, 0xFF, 0xFF, 0x00, 0x40));
    FillBox (mem_dc, 200, 0, 200, 50);

    /* 设置一个半透明的刷子并填充矩形 */
    SetBrushColor (mem_dc, RGBA2Pixel (mem_dc, 0xFF, 0xFF, 0x00, 0x80));
    FillBox (mem_dc, 0, 50, 200, 50);

    /* 设置一个 75% 透明的刷子并填充矩形 */
    SetBrushColor (mem_dc, RGBA2Pixel (mem_dc, 0xFF, 0xFF, 0x00, 0xC0));
    FillBox (mem_dc, 200, 50, 200, 50);
    SetBkMode (mem_dc, BM_TRANSPARENT);

    /* 以半透明的象素点输出文字 */
    SetTextColor (mem_dc, RGBA2Pixel (mem_dc, 0x00, 0x00, 0x00, 0x80));
    TabbedTextOut (mem_dc, 0, 0, "Memory DC with alpha.\n"
                                 "The source DC have alpha per-pixel.");

    /* Blit 到窗口 DC 上 */
    start_tick = GetTickCount ();
    count = 100;
    while (count--) {
        BitBlt (mem_dc, 0, 0, 400, 100, hdc, rand () % 800, rand () % 800);
    }
    end_tick = GetTickCount ();
    TellSpeed (hwnd, start_tick, end_tick, "Alpha Blit", 100);

    /* 删除内存 DC */
    DeleteMemDC (mem_dc);

    /* 具有 Alpha 通道 的内存 DC:32 位,RGB 各占 8 位,无 Alpha 分量 */
    mem_dc = CreateMemDC (400, 100, 32, MEMDC_FLAG_HWSURFACE | MEMDC_FLAG_SRCALPHA | MEMDC_FLAG_SRCCOLORKEY,
                    0x00FF0000, 0x0000FF00, 0x000000FF, 0x00000000);

    /* 输出填充矩形和文本到内存 DC 上 */
    SetBrushColor (mem_dc, RGB2Pixel (mem_dc, 0xFF, 0xFF, 0x00));
    FillBox (mem_dc, 0, 0, 400, 100);
    SetBkMode (mem_dc, BM_TRANSPARENT);
    SetTextColor (mem_dc, RGB2Pixel (mem_dc, 0x00, 0x00, 0xFF));
    TabbedTextOut (mem_dc, 0, 0, "Memory DC with alpha.\n"
                                 "The source DC have alpha per-surface.");

    /* Blit 到窗口 DC 上 */
    start_tick = GetTickCount ();
    count = 100;
    while (count--) {
        /* 设置内存 DC 的 Alpha 通道 */
        SetMemDCAlpha (mem_dc, MEMDC_FLAG_SRCALPHA | MEMDC_FLAG_RLEACCEL, rand () % 256);
        BitBlt (mem_dc, 0, 0, 400, 100, hdc, rand () % 800, rand () % 800);
    }
    end_tick = GetTickCount ();
    TellSpeed (hwnd, start_tick, end_tick, "Alpha Blit", 100);

    /* 填充矩形区域, 并输出文字 */
    FillBox (mem_dc, 0, 0, 400, 100);
    SetBrushColor (mem_dc, RGB2Pixel (mem_dc, 0xFF, 0x00, 0xFF));
    TabbedTextOut (mem_dc, 0, 0, "Memory DC with alpha and colorkey.\n"
                                 "The source DC have alpha per-surface.\n"
                                 "And the source DC have a colorkey, \n"
                                 "and RLE accelerated.");

    /* 设置内存 DC 的透明象素值 */
    SetMemDCColorKey (mem_dc, MEMDC_FLAG_SRCCOLORKEY | MEMDC_FLAG_RLEACCEL,
                    RGB2Pixel (mem_dc, 0xFF, 0xFF, 0x00));
    /* Blit 到窗口 DC 上 */
    start_tick = GetTickCount ();
    count = 100;
    while (count--) {
        BitBlt (mem_dc, 0, 0, 400, 100, hdc, rand () % 800, rand () % 800);
        CHECK_MSG;
    }
    end_tick = GetTickCount ();
    TellSpeed (hwnd, start_tick, end_tick, "Alpha and colorkey Blit", 100);

    /* 删除内存 DC 对象 */
    DeleteMemDC (mem_dc);

4.4 增强的 BITMAP 操作
    新的 GDI 函数增强了 BITMAP 结构,添加了对透明和 Alpha 通道的支持。通过设置 bmType、bmAlpha、bmColorkey 等成员,就可以使得 BITMAP 对象具有某些属性。然后可以利用 FillBoxWithBitmap/Part 函数将 BITMAP 对象绘制到某个 DC 上。你可以将 BITMAP 对象看成是在系统内存中建立的内存 DC 对象,只是不能向这种内存 DC 对象进行绘图输出。下面的示例程序从图象文件中装载一个位图对象,然后设置透明和 Alpha 通道值,最后使用 FillBoxWithBitmap 函数输出到窗口 DC 上:

   int tox = 800, toy = 800;
    int count;
    BITMAP bitmap;
    unsigned int start_tick, end_tick;

    if (LoadBitmap (hdc, &bitmap, "res/icon.bmp"))
        return;

    bitmap.bmType = BMP_TYPE_ALPHACHANNEL;

    /* 位图的 Alpha 混和 */
    start_tick = GetTickCount ();
    count = 1000;
    while (count--) {
        tox = rand() % 800;
        toy = rand() % 800;

        /* 设置随机 Alpha 通道值 */
        bitmap.bmAlpha = rand() % 256;
        /* 显示到窗口 DC 上 */
        FillBoxWithBitmap (hdc, tox, toy, 0, 0, &bitmap);
    }
    end_tick = GetTickCount ();
    TellSpeed (hwnd, start_tick, end_tick, "Alpha Blended Bitmap", 1000);

    bitmap.bmType = BMP_TYPE_ALPHACHANNEL | BMP_TYPE_COLORKEY;
    /*  取第一个象素点值,并设置为透明象素值 */
    bitmap.bmColorKey = GetPixelInBitmap (&bitmap, 0, 0);

    /* 透明及 Alpha 混和 */
    start_tick = GetTickCount ();
    count = 1000;
    while (count--) {
        tox = rand() % 800;
        toy = rand() % 800;

        /*  设置一个随机 Alpha 通道值 */
        bitmap.bmAlpha = rand() % 256;
        /* 显示到窗口 DC 上 */
        FillBoxWithBitmap (hdc, tox, toy, 0, 0, &bitmap);
    }
    end_tick = GetTickCount ();
    TellSpeed (hwnd, start_tick, end_tick, "Alpha Blended Transparent Bitmap", 1000);

    UnloadBitmap (&bitmap);

    你也可以通过 CreateMemDCFromBitmap 函数将某个 BITMAP 对象转换成内存 DC 对象。该函数的原型如下(src/gdi.h):

HDC GUIAPI CreateMemDCFromBitmap (HDC hdc, BITMAP* bmp);

    需要注意的是,从 BITMAP 对象创建的内存 DC 直接使用 BITMAP 对象中的 bmBits 所指向的内存,该内存存在于系统内存,而不是显示内存中。

    和 BITMAP 相关的 MYBITMAP 结构,新的 GDI 也做了一些增强。MYBITMAP 可以看成是设备无关的位图结构,你也可以利用 CreateMemDCFromMyBitmap 函数将一个 MYBITMAP 对象转换成内存 DC。该函数的原型如下(src/gdi.h):

    HDC GUIAPI CreateMemDCFromMyBitmap (HDC hdc, MYBITMAP* mybmp);

    需要注意的是,许多 GAL 引擎不能对系统内存到显示内存的 BitBlt 操作提供硬件加速,所以,FillBoxWithBitmap 函数,以及从 BITMAP 对象或者 MYBITMAP 对象创建的内存 DC 无法通过硬件加速功能快速 BitBlt 到其他 DC 上。如果希望达到这样的效果,可以通过预先创建的建立于显示内存中的 DC 进行快速的 BitBlt 运算。

4.5 新的 GDI 绘图函数
    除了光栅操作意外,还添加了一些有用的 GDI 绘图函数,包括 FillRect、FillCircle 等等,我们将在接下来的开发中,将继续添加诸如椭圆、圆弧、三次样条曲线、多边形填充等高级绘图函数。目前新增的 GDI 函数有:

void GUIAPI FillRect (HDC hdc, int x, int y, int w, int h);
void GUIAPI FillCircle (HDC hdc, int sx, int sy, int r);

BOOL GUIAPI ScaleBitmap (BITMAP* dst, const BITMAP* src);

BOOL GUIAPI GetBitmapFromDC (HDC hdc, int x, int y, int w, int h, BITMAP* bmp);

gal_pixel GUIAPI GetPixelInBitmap (const BITMAP* bmp, int x, int y);
BOOL GUIAPI SetPixelInBitmap (const BITMAP* bmp, int x, int y, gal_pixel pixel);

FillRect 函数填充指定矩形,受当前光栅操作影响。
FillCircle 函数填充指定的圆,受当前光栅操作影响。
ScaleBitmap 函数将源 BITMAP 对象进行伸缩处理。
GetBitmapFromDC 函数将指定矩形范围内的象素复制到 BITMAP 对象中。
GetPixelInBitmap 函数获得 BITMAP 对象中指定位置的象素值。
SetPixelInBitmap 函数设置 BITMAP 对象中指定位置的象素值。
5 其他
    尽管在 1.1.0Pre4 以及其后版本对 MiniGUI 的 GAL 和 GDI 进行了大规模的改造,但在新版本中仍然可以利用老的 GAL 和 GDI 接口,从而提供对低端显示设备的支持。需要注意的是,虽然新 GDI API 当中的许多结构和函数具有相同的名称,但某些函数已经被重新定义。所以,在编写应用程序的时候,要特别注意这一点。比如:新的 mde 演示程序当中,就利用了在 <minigui/config.h> 中定义的 _USE_NEWGAL 宏来判断是否使用新的 GAL 和 GDI 函数,如下所示:

#include <minigui/common.h>
#include <minigui/minigui.h>
#include <minigui/gdi.h>

......
#ifdef _USE_NEWGAL
    SetRasterOperation (hdc, ROP_XOR);
    FillRect (hdc, 0, 0, 200, 200);
#else
    /* Not implemented */
#endif
......


6 小结
    本文重点介绍了在 MiniGUI 1.1.0 版本开发过程中新增的 GAL、GDI 功能和接口。新的 GAL 和 GDI重点针对高端图形应用进行了优化和功能增强,其中包括透明处理、Alpha 混和等高级特性,并且能够对硬件加速功能提供良好支持。本文分别就 GAL 和 GDI 的关系、GAL 的功能特性、GDI 的增强接口等方面较为全面地介绍了新的 GAL 和 GDI 接口。希望能够对程序开发有所帮助。

[目录]


MiniGUI 1.1.0引入的新GDI功能和函数(2)

1 引言
    我们在本系列主题五中曾经详细描述了在 MiniGUI 1.1.0 版本开发过程中添加的新 GDI 功能和函数。这些接口首次出现在版本 1.1.0Pre4 当中。目前 MiniGUI 1.1.0Pre7 版本已经发布,该版本中的新 GDI 接口趋于稳定,相对 1.1.0Pre4 版本而言,又新增了若干高级图形接口。这些接口涉及到直线和曲线生成器、复杂曲线的绘制、封闭曲线填充、复杂区域的创建、直接的显示缓冲区访问、YUV 覆盖和 Gamma 校正等等。本文将就这些主题详细描述各个接口的用法。

2 曲线和填充生成器
    在一般的图形系统中,通常给用户提供若干用于进行直线或者复杂曲线,比如圆弧、椭圆和样条曲线的绘图函数。用户可以通过这些函数进行绘图,但不能利用这些系统中已有的曲线生成算法完成其他的工作。在 MiniGUI 新的 GDI 接口设计当中,我们采用了一种特殊的设计方法来实现曲线和封闭曲线的填充,这种方法非常灵活,而且给用户提供了直接使用系统内部算法的机会:

    1)系统中定义了若干用来生成直线和曲线的函数,我们称之为"曲线生成器";
    2)用户在调用生成器之前,需要定义一个回调函数,并将函数地址传递给曲线生成器,曲线生成器在生成了一个曲线上的点或者封闭曲线中的一条水平填充线时,将调用这个回调函数。
    3)用户可以在回调函数当中完成针对新的点或者新的水平填充线的操作。对 MiniGUI 绘图函数来说,就是完成绘图工作。
    4)因为回调函数在生成器的运行过程中不断调用,为了保持一致的上下文环境,系统允许用户在调用曲线生成器时传递一个表示上下文的指针,生成器将把该指针传递给回调函数。

    下面将分小节讲述目前的 MiniGUI 版本所提供的曲线和填充生成器。

2.1 直线剪切器和直线生成器

直线剪切器和生成器的原型如下:

/* Line clipper */
BOOL GUIAPI LineClipper (const RECT* cliprc, int *_x0, int *_y0, int *_x1, int *_y1);

/* Line generators */
typedef void (* CB_LINE) (void* context, int stepx, int stepy);
void GUIAPI LineGenerator (void* context, int x1, int y1, int x2, int y2, CB_LINE cb);


    直线剪切器并不是生成器,它用于对给定的直线进行剪切操作。cliprc 是给定的直线,而 _x0、_y0、_x1 和 _y1 传递要剪切的直线起始端点,并通过这些指针返回剪切之后的直线起始端点。MiniGUI 内部使用了 Cohen-Sutherland 算法。

LineGenerator 是采用 Breshenham 算法的生成器。该生成器从给定直线的起始端点开始,每生成一个点调用一次 cb 回调函数,并传递上下文 context、以及新的点相对于上一个点的步进值或者差量。比如,传递 stepx =1,stepy = 0 表示新的点比上一个点在 X 轴上前进一步,而在 Y 轴上保持不变。回调函数可以在步进值基础上实现某种程度上的优化。

2.2 圆生成器

MiniGUI 定义的圆生成器原型如下:

/* Circle generator */
typedef void (* CB_CIRCLE) (void* context, int x1, int x2, int y);
void GUIAPI CircleGenerator (void* context, int sx, int sy, int r, CB_CIRCLE cb);

    首先要指定圆心坐标以及半径,并传递上下文信息以及回调函数,每生成一个点,生成器将调用一次 cb 回调函数,并传递三个值:x1、x2 和 y。这三个值实际表示了圆上的两个点:(x1, y) 和 (x2, y)。因为圆的对称性,生成器只要计算圆上的四分之一圆弧点即可得出圆上所有的点。

2.3 椭圆生成器

    椭圆生成器和圆生成器类似,原型如下:

/* Ellipse generator */
typedef void (* CB_ELLIPSE) (void* context, int x1, int x2, int y);
void GUIAPI EllipseGenerator (void* context, int sx, int sy, int rx, int ry, CB_ELLIPSE cb);

    首先要指定椭圆心坐标以及 X 轴和 Y 轴半径,并传递上下文信息以及回调函数,每生成一个点,生成器将调用一次 cb 回调函数,并传递三个值:x1、x2 和 y。这三个值实际表示了椭圆上的两个点:(x1, y) 和 (x2, y)。因为椭圆的对称性,生成器只要计算椭圆上的二分之一圆弧点即可得出椭圆上所有的点。

2.4 圆弧生成器

MiniGUI 定义的圆弧生成器如下所示:

/* Arc generator */
typedef void (* CB_ARC) (void* context, int x, int y);
void GUIAPI ArcGenerator (void* context, int sx, int sy, int r, fixed ang1, fixed ang2, CB_ARC cb);

    首先要指定圆弧的圆心、半径、起始弧度和终止弧度。需要注意的是,起始弧度和终止弧度是采用定点数表示的,而不是浮点数,并且是弧度而不是角度。然后传递 cb 回调函数。每生成一个圆弧上的点,该函数将调用回调函数,并传递新点的坐标值 (x, y)。

    有关定点数的信息,请参阅本系列"主题六:MiniGUI 提供的非 GUI/GDI 接口"一文。

2.5 垂直单调多边形生成器

    通常而言,多边形有凸多边形和凹多边形之分。这里的垂直单调多边形,是为了优化多边形填充算法而针对计算机图形特点而提出的一种特殊多边形,这种多边形的定义如下:

垂直单调多边形是指,多边形的边和计算机屏幕上的所有水平扫描线,只能有一个或者两个交点,不会有更多交点。

图 1 给出了凸多边形、凹多边形和垂直单调多边形的几个示例。

    需要注意的是,凸多边形一定是垂直单调多边形,但垂直单调多边形可以是凹多边形。显然,普通的多边形填充算法需要判断多边形边和每条屏幕扫描线之间的交点个数,而垂直单调多边形则可以免去这一判断,所以可以大大提高多边形填充的速度。

MiniGUI 所定义的垂直单调多边形相关函数原型如下:

/* To determine whether the specified Polygon is Monotone Vertical Polygon */
BOOL GUIAPI PolygonIsMonotoneVertical (const POINT* pts, int vertices);

/* Monotone vertical polygon generator */
typedef void (* CB_POLYGON) (void* context, int x1, int x2, int y);
BOOL GUIAPI MonotoneVerticalPolygonGenerator (void* context, const POINT* pts, int vertices, CB_POLYGON cb);

    PolygonIsMonotoneVertical 用来判断给定的多边形是否是垂直单调多边形,而 MonotoneVerticalPolygonGenerator 函数是垂直多边形生成器。在 MiniGUI 当中,多边形是由组成多边形的顶点来表示的。pts 表示顶点数组,而 vertices 表示顶点个数。生成器生成的实际是填充多边形的每一条水平线,端点为 (x1, y) 和 (x2, y)。

2.6 一般矩形生成器

    MiniGUI 还提供了一般的矩形生成器,该生成器可以处理凸多边形,也可以处理凹多边形。原型如下:

/* General polygon generator */
typedef void (* CB_POLYGON) (void* context, int x1, int x2, int y);
BOOL GUIAPI PolygonGenerator (void* context, const POINT* pts, int vertices, CB_POLYGON cb);

    和垂直单调多边形生成器一样,该函数生成的是填充多边形的每一条水平扫描线:x1 是水平线的起始X坐标;x2 是水平线的终止 X 坐标;y 是水平线的 Y 坐标值。

2.7 填注生成器

    填注(flood filling)生成器比较复杂。这个函数在 MiniGUI 内部用于 FloodFill 函数。我们知道,FloodFill 函数从给定的起始位置开始,以给定的颜色向四面八方填充某个区域(像水一样蔓延,因此叫 Flood Filling),一直到遇到与给定起始位置的象素值不同的点为止。因此,在这一过程中,我们需要两个回调函数,一个回调函数用来判断蔓延过程中遇到的点的象素值是否和起始点相同,另外一个回调函数用来生成填充该区域的水平扫描线。在进行绘图时,该函数比较的是象素值,但实际上,该函数也可以比较任何其他值,从而完成特有的蔓延动作。这就是将填注生成器单独出来的初衷。MiniGUI 如下定义填注生成器:

/* General Flood Filling generator */
typedef BOOL (* CB_EQUAL_PIXEL) (void* context, int x, int y);
typedef void (* CB_FLOOD_FILL) (void* context, int x1, int x2, int y);
BOOL GUIAPI FloodFillGenerator (void* context, const RECT* src_rc, int x, int y,
                CB_EQUAL_PIXEL cb_equal_pixel, CB_FLOOD_FILL cb_flood_fill);

    cb_equal_pixel 被调用,以便判断目标点的象素值是否和起始点一样,起始点的象素值可以通过 context 来传递。cb_flood_fill 函数用来填充一条扫描线,传递的是水平扫描线的端点,即(x1, y) 和 (x2, y)。

2.8 曲线和填充生成器的用法

    曲线和填充生成器的用法非常简单。为了对曲线和填充生成器有个更好的了解,我们首先看 MiniGUI 内部是如何使用曲线和填充生成器的。

下面的程序段来自 MiniGUI 的 FloodFill 函数(src/newgdi/flood.c):

static void _flood_fill_draw_hline (void* context, int x1, int x2, int y)
{
    PDC pdc = (PDC)context;
    RECT rcOutput = {MIN (x1, x2), y, MAX (x1, x2) + 1, y + 1};

    ENTER_DRAWING (pdc, rcOutput);
    _dc_draw_hline_clip (context, x1, x2, y);
    LEAVE_DRAWING (pdc, rcOutput);
}

static BOOL equal_pixel (void* context, int x, int y)
{
    gal_pixel pixel = _dc_get_pixel_cursor ((PDC)context, x, y);

    return ((PDC)context)->skip_pixel == pixel;
}

/* FloodFill
* Fills an enclosed area (starting at point x, y).
*/
BOOL GUIAPI FloodFill (HDC hdc, int x, int y)
{
    PDC pdc;
    BOOL ret = TRUE;

    if (!(pdc = check_ecrgn (hdc)))
        return TRUE;

    /* hide cursor tempororily */
    ShowCursor (FALSE);

    coor_LP2SP (pdc, &x, &y);

    pdc->cur_pixel = pdc->brushcolor;
    pdc->cur_ban = NULL;

    pdc->skip_pixel = _dc_get_pixel_cursor (pdc, x, y);

    /* does the start point have a equal value? */
    if (pdc->skip_pixel == pdc->brushcolor)
        goto equal_pixel;

    ret = FloodFillGenerator (pdc, &pdc->DevRC, x, y, equal_pixel, _flood_fill_draw_hline);

equal_pixel:
    UNLOCK_GCRINFO (pdc);

    /* Show cursor */
    ShowCursor (TRUE);

    return ret;
}

    该函数在经过一些必要的初始化工作之后,调用 FloodFillGenerator 函数,并传递了上下文 pdc (pdc 是 MiniGUI 内部表示 DC 的数据结构)和两个回调函数地址:equal_pixel 和 _flood_fill_draw_hline 函数。在这之前,该函数获得了起始点的象素值,并保存在了pdc->skip_pixel 当中。equal_pixel 函数获得给定点的象素值,然后返回与 pdc->skip_pixel 相比较之后的值;_flood_fill_draw_hline 函数调用内部函数进行水平线的绘制。

    读者可以看到,这种简单的生成器实现方式,能够大大降低代码复杂度,提高代码的重用能力。有兴趣的读者可以比较 MiniGUI 新老 GDI 接口的 LineTo 函数实现,相信能够得出一样的结论。

    当然设计生成器的目的主要还是为方便用户使用。比如,你可以利用 MiniGUI 内部的曲线生成器完成自己的工作。下面的示例假定你使用圆生成器绘制一个线宽为 4 象素的圆:

static void draw_circle_pixel (void* context, int x1, int x2, int y)
{
    HDC hdc = (HDC) context;

    /* 以圆上的每个点为圆心,填充半径为 2 的圆。*/
    FillCircle (hdc, x1, y, 2);
    FillCircle (hdc, x2, y, 2);
}

void DrawMyCircle (HDC hdc, int x, int y, int r, gal_pixel pixel)
{
    gal_pixel old_brush;

    old_bursh = SetBrushColor (hdc, pixle);

    /* 调用圆生成器 */
    CircleGenerator ((void*)hdc, x, y, r, draw_circle_pixel);

    /* 恢复旧的画刷颜色 */
    SetBrushColor (hdc, old_brush);
}


    从上面的例子可以看出,曲线和填充生成器的用法极其简单,而且结构清晰明了。读者在自己的开发过程中,也可以学习这种方法。

3 绘制复杂曲线

基于 2 中描述的曲线生成器,MiniGUI 提供了如下基本的曲线绘制函数:

void GUIAPI MoveTo (HDC hdc, int x, int y);
void GUIAPI LineTo (HDC hdc, int x, int y);
void GUIAPI Rectangle (HDC hdc, int x0, int y0, int x1, int y1);
void GUIAPI PollyLineTo (HDC hdc, const POINT* pts, int vertices);
void GUIAPI SplineTo (HDC hdc, const POINT* pts);
void GUIAPI Circle (HDC hdc, int sx, int sy, int r);
void GUIAPI Ellipse (HDC hdc, int sx, int sy, int rx, int ry);
void GUIAPI Arc (HDC hdc, int sx, int sy, int r, fixed ang1, fixed ang2);

MoveTo 将当前画笔的起始点移动到给定点(x, y),以逻辑坐标指定。
LineTo 从当前画笔点画直线到给定点(x, y),以逻辑坐标指定。
Rectangle 函数画顶点为(x0, y0)和(x1, y0)的矩形。
PollyLineTo 函数利用 LineTo 函数画折线。pts 指定了折线的各个端点,vertices 指定了折线端点个数。
SplineTo 函数利用 LineTo 函数画三次样条曲线。需要注意的是,必须传递四个点才能惟一确定一条样条曲线,也就是说,pts 是一个指向包含 4 个 POINT 结构数组的指针。
Circle 函数绘制圆,圆心为 (sx, sy),半径为 r,以逻辑坐标指定。
Ellipse 函数绘制椭圆,椭圆心为(sx, sy),X 轴半径为 rx,Y 轴半径为 ry。
Arc 函数绘制圆弧,(sx, sy) 指定了圆心,r 指定半径,ang1 和 ang2 指定圆弧的起始弧度和终止弧度。需要注意的是,ang1 和 ang2 是以定点数形式指定的。
作为示例,我们看 Circle 和 Ellipse 函数的用法。假定给定了两个点,pts[0] 和 pts[1],其中 pts[0] 是圆心或者椭圆心,而 pts[1] 是圆或者椭圆外切矩形的一个顶点。下面的程序段绘制由这两个点给定的圆或者椭圆:

           int rx = ABS (pts[1].x - pts[0].x);
            int ry = ABS (pts[1].y - pts[0].y);

            if (rx == ry)
                Circle (hdc, pts[0].x, pts[0].y, rx);
            else
                Ellipse (hdc, pts[0].x, pts[0].y, rx, ry);

4 封闭曲线填充
    MiniGUI 目前提供了如下的封闭曲线填充函数:

void GUIAPI FillBox (HDC hdc, int x, int y, int w, int h);
void GUIAPI FillCircle (HDC hdc, int sx, int sy, int r);
void GUIAPI FillEllipse (HDC hdc, int sx, int sy, int rx, int ry);
void GUIAPI FillSector (HDC hdc, int sx, int sy, int r, int ang1, int ang2);
BOOL GUIAPI FillPolygon (HDC hdc, const POINT* pts, int vertices);
BOOL GUIAPI FloodFill (HDC hdc, int x, int y);

FillBox 函数填充指定的矩形。该矩形左上角顶点为(x, y),宽度为 w,高度为 h,以逻辑坐标指定。
FillCircle 函数填充指定的圆。圆心为(sx, xy),半径为 r,以逻辑坐标指定。
FillEllips 函数填充指定的椭圆。椭圆心为(sx, sy),X 轴半径为 rx,Y 轴半径为 ry。
FillSector 函数填充由圆弧和两条半径形成的扇形。圆心为(x, y),半径为 r,起始弧度为 ang1,终止弧度为 ang2。
FillPolygon 函数填充多边形。pts 表示多边形各个顶点,vertices 表示多边形顶点个数。
FloodFill 从指定点(x, y)开始填注。

    需要注意的是,所有填充函数使用当前画刷属性(颜色),并且受当前光栅操作的影响。

    下面的例子说明了如何使用 FillCircle 和 FillEllipse 函数填充圆或者椭圆。假定给定了两个点,pts[0] 和 pts[1],其中 pts[0] 是圆心或者椭圆心,而 pts[1] 是圆或者椭圆外切矩形的一个顶点。

           int rx = ABS (pts[1].x - pts[0].x);
            int ry = ABS (pts[1].y - pts[0].y);

            if (rx == ry)
                FillCircle (hdc, pts[0].x, pts[0].y, rx);
            else
                FillEllipse (hdc, pts[0].x, pts[0].y, rx, ry);


5 建立复杂区域

    除了利用填充生成器进行填充绘制以外,我们还可以使用填充生成器建立由封闭曲线包围的复杂区域。我们知道,MiniGUI 当中的区域是由互不相交的矩形组成的,并且满足 x-y-banned 的分布规则。利用上述的多边形或者封闭曲线生成器,可以将每条扫描线看成是组成区域的高度为 1 的一个矩形,这样,我们可以利用这些生成器建立复杂区域。MiniGUI 利用现有的封闭曲线生成器,实现了如下的复杂区域生成函数:

BOOL GUIAPI InitCircleRegion (PCLIPRGN dst, int x, int y, int r);
BOOL GUIAPI InitEllipseRegion (PCLIPRGN dst, int x, int y, int rx, int ry);
BOOL GUIAPI InitPolygonRegion (PCLIPRGN dst, const POINT* pts, int vertices);
BOOL GUIAPI InitSectorRegion (PCLIPRGN dst, const POINT* pts, int vertices);

    利用这些函数,我们可以将某个区域分别初始化为圆、椭圆、多边形和扇形区域。然后,可以利用这些区域进行点击测试(PtInRegion 和 RectInRegion),或者选择到 DC 当中作为剪切域,从而获得特殊显示效果。

6 直接访问显示缓冲区

    在新的 GDI 接口中,我们添加了用来直接访问显示缓冲区的函数,原型如下:

Uint8* GUIAPI LockDC (HDC hdc, const RECT* rw_rc, int* width, int* height, int* pitch);
void GUIAPI UnlockDC (HDC hdc);

    LockDC 函数锁定给定 HDC 的指定矩形区域(由矩形 rw_rc指定,设备坐标),然后返回缓冲区头指针。当 width、height、pitch 三个指针不为空时,该函数将返回锁定之后的矩形有效宽度、有效高度和每扫描线所占的字节数。
    UnlockDC 函数解开已锁定的 HDC。
    锁定一个 HDC 意味着 MiniGUI 进入以互斥方式访问显示缓冲区的状态。如果被锁定的 HDC 是一个屏幕 DC(即非内存 DC),则该函数将在必要时隐藏鼠标光标,并锁定 HDC 对应的全局剪切域。在锁定一个 HDC 之后,程序可通过该函数返回的指针对锁定区域进行访问。需要注意的是,不能长时间锁定一个 HDC,也不应该在锁定一个 HDC 时进行其他额外的系统调用。

    假定以锁定矩形左上角为原点建立坐标系,X 轴水平向右,Y 轴垂直向下,则可以通过如下的公式计算该坐标系中(x, y)点对应的缓冲区地址(假定该函数返回的指针值为 frame_buffer):

   Uint8* pixel_add = frame_buffer + y * (*pitch) + x * GetGDCapability (hdc, GDCAP_BPP);

根据该 HDC 的颜色深度,就可以对该象素进行读写操作。作为示例,下面的程序段随机填充锁定区域:

   int i, width, height, pitch;
    RECT rc = {0, 0, 200, 200};
    int bpp = GetGDCapability (hdc, GDCAP_BPP);
    Uint8* frame_buffer = LockDC (hdc, &rc, &width, &height, &pitch);
    Uint8* row = frame_buffer;

    for (i = 0; i < *height; i++) {
        memset (row, rand ()%0x100, *width * bpp);
        row += *pitch;
    }

    UnlockDC (hdc);


7 YUV 覆盖和 Gamma 校正

    为了增强 MiniGUI 对多媒体的支持,我们增加了对 YUV 覆盖(Overlay)和 Gamma 校正的支持。

7.1 YUV 覆盖(Overlay)

    多媒体领域中,尤其在涉及到 MPEG 播放时,通常使用 YUV 颜色空间来表示颜色,如果要在屏幕上显示一副 MPEG 解压之后的图片,则需要进行 YUV 颜色空间到 RGB 颜色空间的转换。YUV 覆盖最初来自一些显示芯片的加速功能。这种显示芯片能够在硬件基础上完成 YUV 到 RGB 的转换,免去软件转换带来的性能损失。在这种显示芯片上建立了 YUV 覆盖之后,可以直接将 YUV 信息写入缓冲区,硬件能够自动完成 YUV 到 RGB 的转换,从而在 RGB 显示器上显示出来。在不支持 YUV 覆盖的显示芯片上,MiniGUI 也能够通过软件实现 YUV 覆盖,这时,需要调用 DisplayYUVOverlay 函数将 YUV 信息转换并缩放显示在建立 YUV 覆盖的 DC 设备上。

MiniGUI 提供的 YUV 覆盖操作函数原型如下:

/***************************** YUV overlay support ***************************/
/* 最常见的视频覆盖格式.
*/
#define GAL_YV12_OVERLAY  0x32315659    /* Planar mode: Y + V + U  (3 planes) */
#define GAL_IYUV_OVERLAY  0x56555949    /* Planar mode: Y + U + V  (3 planes) */
#define GAL_YUY2_OVERLAY  0x32595559    /* Packed mode: Y0+U0+Y1+V0 (1 plane) */
#define GAL_UYVY_OVERLAY  0x59565955    /* Packed mode: U0+Y0+V0+Y1 (1 plane) */
#define GAL_YVYU_OVERLAY  0x55595659    /* Packed mode: Y0+V0+Y1+U0 (1 plane) */

/* 该函数创建一个视频输出覆盖
*/
GAL_Overlay* GUIAPI CreateYUVOverlay (int width, int height,
                Uint32 format, HDC hdc);

/* 锁定覆盖进行直接的缓冲区读写,结束后解锁 */
int GAL_LockYUVOverlay (GAL_Overlay *overlay);
void GAL_UnlockYUVOverlay (GAL_Overlay *overlay);

#define LockYUVOverlay GAL_LockYUVOverlay
#define UnlockYUVOverlay GAL_UnlockYUVOverlay

/* 释放视频覆盖 */
void GAL_FreeYUVOverlay (GAL_Overlay *overlay);
#define FreeYUVOverlay GAL_FreeYUVOverlay

/* 将视频覆盖传送到指定 DC 设备上。该函数能够进行 2 维缩放
*/
void GUIAPI DisplayYUVOverlay (GAL_Overlay* overlay, const RECT* dstrect);

有关视频格式的信息,可参见:

http://www.webartz.com/fourcc/indexyuv.htm

有关颜色空间的相互关系的息,可参见:

http://www.neuro.sfc.keio.ac.jp/~aly/polygon/info/color-space-faq.html

7.2 Gamma 校正

    Gamma 校正通过为 RGB 颜色空间的每个颜色通道设置 Gamma 因子,来动态调整 RGB 显示器上的实际 RGB 效果。需要注意的是,Gamma 校正需要显示芯片的硬件支持。

    应用程序可以通过 SetGamma 函数设置 RGB 三个颜色通道的 Gamma 校正值。该函数原型如下:

int GAL_SetGamma (float red, float green, float blue);
#define SetGamma GAL_SetGamma

    线性 Gamma 校正值的范围在 0.1 到 10.0 之间。如果硬件不支持 Gamma 校正,该函数将返回 -1。

    应用程序也可以通过 SetGammaRamp 函数设置 RGB 三个颜色通道的非线性 Gamma 校正值。该函数原型如下:

int GAL_SetGammaRamp (Uint16 *red, Uint16 *green, Uint16 *blue);
#define SetGammaRamp GAL_SetGammaRamp

int GAL_GetGammaRamp (Uint16 *red, Uint16 *green, Uint16 *blue);
#define GetGammaRamp GAL_GetGammaRamp

    函数 SetGammaRamp 实际设置的是每个颜色通道的 Gamma 转换表,每个表由 256 个值组成,表示设置值和实际值之间的对应关系。当设置屏幕上某个象素的 RGB 分别为 R、G、B 时,实际在显示器上获得的象素 RGB 值分别为:red[R]、green[G]、blue[B]。如果硬件不支持 Gamma 校正,该函数将返回 -1。

    函数 GetGammaRamp 获得当前的 Gamma 转换表。

    Gamma 校正的最初目的,是为了能够在显示器上精确还原一副图片。Gamma 值在某种程度上表示的是某个颜色通道的对比度变化。但 Gamma 在多媒体和游戏程序中有一些特殊用途――通过 Gamma 校正,可以方便地获得对比度渐进效果。

8 小结

    本文描述了自 MiniGUI 1.1.0Pre4 版本发布以来新增的 GDI 接口。这些接口涉及到曲线和填充生成器、复杂曲线的绘制、封闭曲线填充、复杂区域的创建、直接访问 FrameBuffer、YUV 覆盖和 Gamma 校正等等。通过本文的介绍,相信读者能够对 MiniGUI 的新 GDI 接口有一个更加全面的认识。

[目录]


MiniGUI 提供的非 GUI/GDI 接口

1 引言
    一般而言,GUI 系统的应用程序编程接口主要集中于窗口、消息队列、图形设备等相关方面。但因为 GUI 系统在处理系统事件时通常会提供自己的机制,而这些机制往往会和操作系统本身提供的机制不相兼容。比如,MiniGUI 提供了消息循环机制,而应用程序的结构一般是消息驱动的;也就是说,应用程序通过被动接收消息来工作。但很多情况下,应用程序需要主动监视某个系统事件,比如在 UNIX 操作系统中,可以通过 select 系统调用监听某个文件描述符上是否有可读数据。这样,如何将 MiniGUI 的消息队列机制和现有操作系统的其他机制融合在一起,就成了一个较为困难的问题。本文将讲述几种解决这一问题的方法。

    我们知道,MiniGUI-Lite 采用 UNIX Domain Socket 实现客户程序和服务器程序之间的交互。应用程序也可以利用这一机制,完成自己的通讯任务――客户向服务器提交请求,而服务器完成对客户的请求处理并应答。一方面,在 MiniGUI-Lite 的服务器程序中,你可以扩展这一机制,注册自己的请求处理函数,完成定制的请求/响应通讯任务。另一方面,MiniGUI-Lite 当中也提供了若干用来创建和操作 UNIX Domain Socket 的函数,任何 MiniGUI-Lite 的应用程序都可以建立 UNIX Domain Socket,并完成和其他 MiniGUI-Lite 应用程序之间的数据交换。本文将举例讲述如何利用 MiniGUI-Lite 提供的函数完成此类通讯任务。

    嵌入式 Linux 系统现在能够在许多不同架构的硬件平台上运行,MiniGUI 也能够在这些硬件平台上运行。但由于许多硬件平台具有和其他硬件平台不同的特性,比如说,常见的 CPU 是 Little Endian 的,而某些 CPU 则是 Big Endian 的。这要求我们在编写代码,尤其是文件 I/O 相关代码时,必须编写可移植代码,以便适合具有不同架构的平台。本文将描述 MiniGUI 为应用程序提供的可移植性函数及其用法。

    除了与上述内容相关的函数之外,MiniGUI 还提供了其他一些函数,本文最后部分将描述这些函数的用途和用法,包括配置文件读写以及定点数运算。

2 MiniGUI-Lite和 select 系统调用
    我们知道,在 MiniGUI-Lite 之上运行的应用程序只有一个消息队列。应用程序在初始化之后,会建立一个消息循环,然后不停地从这个消息队列当中获得消息并处理,直到接收到 MSG_QUIT 消息为止。应用程序的窗口过程在处理消息时,要在处理完消息之后立即返回,以便有机会获得其他的消息并处理。现在,如果应用程序在处理某个消息时监听某个文件描述符而调用 select 系统调用,就有可能会出现问题――因为 select 系统调用可能会长时间阻塞,而由 MiniGUI-Lite 服务器发送给客户的事件得不到及时处理。这样,消息驱动的方式和 select 系统调用就难于很好地融合。在 MiniGUI-Threads 中,因为每个线程都有自己相应的消息队列,而系统消息队列是由单独运行的 desktop 线程管理的,所以任何一个应用程序建立的线程都可以长时间阻塞,从而可以调用类似 select 的系统调用。但在 MiniGUI-Lite 当中,如果要监听某个应用程序自己的文件描述符事件,必须进行恰当的处理,以避免长时间阻塞。

    在 MiniGUI-Lite 当中,有几种解决这一问题的办法:

    在调用 select 系统调用时,传递超时值,保证 select 系统调用不会长时间阻塞。
    设置定时器,定时器到期时,利用 select 系统调用查看被监听的文件描述符。如果没有相应的事件发生,则立即返回,否则进行读写操作。
    利用 MiniGUI-Lite 提供的 RegisterListenFD 函数在系统中注册监听文件描述符,并在被监听的文件描述符上发生指定的事件时,向某个窗口发送 MSG_FDEVENT 消息。

    由于前两种解决方法比较简单,这里我们重点讲述的第三种解决办法。MiniGUI-Lite 为应用程序提供了如下两个函数及一个宏:
#define MAX_NR_LISTEN_FD   5

/* Return TRUE if all OK, and FALSE on error. */
BOOL GUIAPI RegisterListenFD (int fd, int type, HWND hwnd, void* context);

/* Return TRUE if all OK, and FALSE on error. */
BOOL GUIAPI UnregisterListenFD (int fd);

MAX_NR_LISTEN_FD 宏定义了系统能够监听的最多文件描述符数,默认定义为 5。
RegisterListenFD 函数在系统当中注册一个需要监听的文件描述符,并指定监听的事件类型(type 参数,可取 POLLIN、POLLOUT 或者 POLLERR),接收 MSG_FDEVENT 消息的窗口句柄以及一个上下文信息。
UnregisterListenFD 函数注销一个被注册的监听文件描述符。

在应用程序使用RegisterListenFD 函数注册了被监听的文件描述符之后,当指定的事件发生在该文件描述符上时,系统会将 MSG_FDEVENT 消息发送到指定的窗口,应用程序可在窗口过程中接收该消息并处理。MiniGUI 中的 libvcongui 就利用了上述函数监听来自主控伪终端上的可读事件,如下面的程序段所示(vcongui/vcongui.c):
    ...

    /* 注册主控伪终端伪监听文件描述符 */
    RegisterListenFD (pConInfo->masterPty, POLLIN, hMainWnd, 0);

    /* 进入消息循环 */
    while (!pConInfo->terminate && GetMessage (&Msg, hMainWnd)) {
        DispatchMessage (&Msg);
    }
    /* 注销监听文件描述符 */
    UnregisterListenFD (pConInfo->masterPty);

    ...

/* 虚拟控制台的窗口过程 */
static int VCOnGUIMainWinProc (HWND hWnd, int message, WPARAM wParam, LPARAM lParam)
{
    PCONINFO pConInfo;

    pConInfo = (PCONINFO)GetWindowAdditionalData (hWnd);
    switch (message) {

         ...

       /* 接收到 MSG_FDEVENT 消息,则处理主控伪终端上的输入数据 */
        case MSG_FDEVENT:
            ReadMasterPty (pConInfo);
        break;

        ...
    }

    /* 调用默认窗口过程 */
    if (pConInfo->DefWinProc)
        return (*pConInfo->DefWinProc)(hWnd, message, wParam, lParam);
    else
        return DefaultMainWinProc (hWnd, message, wParam, lParam);
}

    在 3.2 节当中,我们还可以看到RegisterListenFD 函数的使用。显然,通过这种简单的注册监听文件描述符的接口,MiniGUI-Lite 程序能够方便地利用底层的消息机制完成对异步事件的处理。

3 MiniGUI-Lite 与进程间通讯

3.1 简单请求/应答处理
我们知道,MiniGUI-Lite 利用了 UNIX Domain Socket 实现服务器和客户程序之间的通讯。为了实现客户和服务器之间的简单方便的通讯,MiniGUI-Lite 中定义了一种简单的请求/响应结构。客户程序通过指定的结构将请求发送到服务器,服务器处理请求并应答。在客户端,一个请求定义如下(include/gdi.h):
typedef struct tagREQUEST {
    int id;
    const void* data;
    size_t len_data;
} REQUEST;
typedef REQUEST* PREQUEST;

其中,id 是用来标识请求类型的整型数,data 是发送给该请求的关联数据,len_data 则是数据的长度。客户在初始化 REQUEST 结构之后,就可以调用 cli_request 向服务器发送请求,并等待服务器的应答。该函数的原型如下。
/* send a request to server and wait reply */
int cli_request (PREQUEST request, void* result, int len_rslt);

    服务器程序(即 mginit)会在自己的消息循环当中获得来自客户的请求,并进行处理,最终会将处理结果发送给客户。

    在上述这种简单的客户/服务器通讯中,客户和服务器必须就每个请求类型达成一致,也就是说,客户和服务器必须了解每种类型请求的数据含义并进行恰当的处理。

    MiniGUI-Lite 利用上述这种简单的通讯方法,实现了若干系统级的通讯任务:

    鼠标光标的管理。鼠标光标是一个全局资源,当客户需要创建或者销毁鼠标光标,改变鼠标光标的形状、位置,显示或者隐藏鼠标时,就发送请求到服务器,服务器程序完成相应任务并将结果发送给客户。
层及活动客户管理。当客户查询层的信息,新建层,加入某个已有层,或者设置层中的活动客户时,通过该接口发送请求到服务器。
    其他一些系统级的任务。比如在新的 GDI 接口中,服务器程序统一管理显示卡中可能用来建立内存 DC 的显示内存,当客户要申请建立在显示内存中的内存 DC 时,就会发送请求到服务器。

    为了让应用程序也能够通过这种简单的方式实现客户和服务器之间的通讯,服务器程序可以注册一些定制的请求处理函数,然后客户就可以向服务器发送这些请求。为此,MiniGUI-Lite 提供了如下接口:

#define MAX_SYS_REQID           0x0010
#define MAX_REQID               0x0018

/*
* Register user defined request handlers for server
* Note that user defined request id should larger than MAX_SYS_REQID
*/
typedef int (* REQ_HANDLER) (int cli, int clifd, void* buff, size_t len);
BOOL GUIAPI RegisterRequestHandler (int req_id, REQ_HANDLER your_handler);
REQ_HANDLER GUIAPI GetRequestHandler (int req_id);

    服务器可以通过调用RegisterRequestHandler 函数注册一些请求处理函数。注意请求处理函数的原型由REQ_HANDLER 定义。还要注意系统定义了MAX_SYS_REQID 和 MAX_REQID 这两个宏。MAX_REQID 是能够注册的最大请求 ID 号,而 MAX_SYS_REQID 是系统内部使用的最大的请求 ID 号,也就是说,通过RegisterRequestHandler 注册的请求 ID 号,必须大于 MAX_SYS_REQID 而小于或等于 MAX_REQID。

    作为示例,我们假设服务器替客户计算两个整数的和。客户发送两个整数给服务器,而服务器将两个整数的和发送给客户。下面的程序段在服务器程序中运行,在系统中注册了一个请求处理函数:
typedef struct TEST_REQ
{
   int a, b;
} TEST_REQ;

static int send_reply (int clifd, void* reply, int len)
{
    MSG reply_msg = {HWND_INVALID, 0};

    /* 发送一个空消息接口给客户,以便说明这是一个请求的应答 */
    if (sock_write (clifd, &reply_msg, sizeof (MSG)) < 0)
        return SOCKERR_IO;

    /* 将结果发送给客户 */
    if (sock_write (clifd, reply, len) < 0)
        return SOCKERR_IO;

    return SOCKERR_OK;
}

static int test_request (int cli, int clifd, void* buff, size_t len)
{
    int ret_value = 0;
    TEST_REQ* test_req = (TEST_REQ*)buff;

    ret_value = test_req.a + test_req.b;

    return send_reply (clifd, &ret_value, sizeof (int));
}

...
     RegisterRequestHandler (MAX_SYS_REQID + 1, test_request);
...

而客户程序可以通过如下的程序段向客户发送一个请求获得两个整数的和:
        REQUEST req;
        TEST_REQ test_req = {5, 10};
        int ret_value;

        req.id = MAX_SYS_REQID + 1;
        req.data = &rest_req;
        req.len_data = sizeof (TEST_REQ);

        cli_request (&req, &ret_value, sizeof (int));
        printf ("the returned value: %d\n", ret_value);    /* ret_value 的值应该是 15 */

    读者已经看到,通过这种简单的请求/应答技术,MiniGUI-Lite 客户程序和服务器程序之间可以建立一种非常方便的进程间通讯机制。但这种技术也有一些缺点,比如受到 MAX_REQID 大小的影响,通讯机制并不是非常灵活,而且请求只能发送给MiniGUI-Lite 的服务器程序(即 mginit)处理等等。

3.2 复杂的 UNIX Domain Socket 封装
    为了解决上述简单请求/应答机制的不足,MiniGUI-Lite 也提供了经过封装的 UNIX Domain Socket 处理函数。这些函数的接口原型如下(include/minigui.h):
/* Used by server to create a listen socket.
* Name is the name of listen socket.
* Please located the socket in /var/tmp directory. */

/* Returns fd if all OK, -1 on error. */
int serv_listen (const char* name);

/* Wait for a client connection to arrive, and accept it.
* We also obtain the client's pid and user ID from the pathname
* that it must bind before calling us. */

/* returns new fd if all OK, < 0 on error */
int serv_accept (int listenfd, pid_t *pidptr, uid_t *uidptr);

/* Used by clients to connect to a server.
* Name is the name of the listen socket.
* The created socket will located at the directory /var/tmp,
* and with name of '/var/tmp/xxxxx-c', where 'xxxxx' is the pid of client.
* and 'c' is a character to distinguish diferent projects.
* MiniGUI use 'a' as the project character.
*/

/* Returns fd if all OK, -1 on error. */
int cli_conn (const char* name, char project);

#define SOCKERR_IO          -1
#define SOCKERR_CLOSED      -2
#define SOCKERR_INVARG      -3
#define SOCKERR_OK          0

/* UNIX domain socket I/O functions. */

/* Returns SOCKERR_OK if all OK, < 0 on error.*/
int sock_write_t (int fd, const void* buff, int count, unsigned int timeout);
int sock_read_t (int fd, void* buff, int count, unsigned int timeout);

#define sock_write(fd, buff, count) sock_write_t(fd, buff, count, 0)
#define sock_read(fd, buff, count) sock_read_t(fd, buff, count, 0)

    上述函数是 MiniGUI-Lite 用来建立系统内部使用的 UNIX Domain Socket 并进行数据传递的函数,是对基本套接字系统调用的封装。这些函数的功能描述如下:

serv_listen:服务器调用该函数建立一个监听套接字,并返回套接字文件描述符。建议将服务器监听套接字建立在 /var/tmp/ 目录下。
serv_accept:服务器调用该函数接受来自客户的连接请求。
cli_conn:客户调用该函数连接到服务器,其中 name 是客户的监听套接字。该函数为客户建立的套接字将保存在 /var/tmp/ 目录中,并且以 -c 的方式命名,其中 c 是用来区别不同套接字通讯用途的字母,由 project 参数指定。MiniGUI-Lite 内部使用了 'a',所以由应用程序建立的套接字,应该使用除 'a' 之外的字母。
sock_write_t:在建立并连接之后,客户和服务器之间就可以使用 sock_write_t 函数和 sock_read_t 函数进行数据交换。sock_write_t 的参数和系统调用 write 类似,但可以传递进入一个超时参数,注意该参数以 10ms 为单位,为零时超时设置失效,且超时设置只在 mginit 程序中有效。
sock_read_t:sock_read_t 的参数和系统调用 read类似,但可以传递进入一个超时参数,注意该参数以 10ms 为单位,为零时超时设置失效,且超时设置只在 mginit 程序中有效。

    下面的代码演示了作为服务器的程序如何利用上述函数建立一个监听套接字:
#define LISTEN_SOCKET    "/var/tmp/mysocket"

static int listen_fd;

BOOL listen_socket (HWND hwnd)
{
    if ((listen_fd = serv_listen (LISTEN_SOCKET)) < 0)
        return FALSE;
    return RegisterListenFD (fd, POLL_IN, hwnd, NULL);
}

    当服务器接收到来自客户的连接请求是,服务器的 hwnd 窗口将接收到 MSG_FDEVENT 消息,这时,服务器可接受该连接请求:
int MyWndProc (HWND hwnd, int message, WPARAM wParam, LPARAM lParam)
{
    switch (message) {

        ...

        case MSG_FDEVENT:
             if (LOWORD (wParam) == listen_fd) { /* 来自监听套接字 */
                  pid_t pid;
                  uid_t uid;
                  int conn_fd;
                  conn_fd = serv_accept (listen_fd, &pid, &uid);
                  if (conn_fd >= 0) {
                       RegisterListenFD (conn_fd, POLL_IN, hwnd, NULL);
                  }
             }
             else { /* 来自已连接套接字 */
                   int fd = LOWORD(wParam);
                   /* 处理来自客户的数据 */
                   sock_read_t (fd, ...);
                   sock_write_t (fd, ....);
             }
        break;

        ...

    }
}

    上面的代码中,服务器将连接得到的新文件描述符也注册为监听描述符,因此,在 MSG_FDEVENT 消息的处理中,应该判断导致 MSG_FDEVENT 消息的文件描述符类型,并做适当的处理。

在客户端,当需要连接到服务器时,可通过如下代码:
    int conn_fd;

    if ((conn_fd  = cli_conn (LISTEN_SOCKET, 'b')) >= 0) {
        /* 向服务器发送请求 */
        sock_write_t (fd, ....);
        /* 获取来自服务器的处理结果 */
        sock_read_t (fd, ....);
    }

4 编写可移植代码
    我们知道,许多嵌入式系统所使用的 CPU 具有和普通台式机 CPU 完全不同的构造和特点。但有了操作系统和高级语言,可以最大程度上将这些不同隐藏起来。只要利用高级语言编程,编译器和操作系统能够帮助程序员解决许多和 CPU 构造及特点相关的问题,从而节省程序开发时间,并提高程序开发效率。然而某些 CPU 特点却是应用程序开发人员所必须面对的,这其中就有如下几个需要特别注意的方面:

    字节顺序。一般情况下,我们接触到的 CPU 在存放多字节的整数数据时,将低位字节存放在低地址单元中,比如常见的 Intel x86 系列 CPU。而某些 CPU 采用相反的字节顺序。比如在嵌入式系统中使用较为广泛的 PowerPC 就将低位字节存放在高地址单元中。前者叫 Little Endian 系统;而后者叫 Big Endian 系统。
在某些平台上的 Linux 内核,可能缺少某些高级系统调用,最常见的就是与虚拟内存机制相关的系统调用。在某些 CPU 上运行的 Linux 操作系统,因为 CPU 能力的限制,无法提供虚拟内存机制,基于虚拟内存实现的某些 IPC 机制就无法正常工作。比如在某些缺少 MMU 单元的 CPU 上,就无法提供 System V IPC 机制中的共享内存。

    为了编写具有最广泛适应性的可移植代码,应用程序开发人员必须注意到这些不同,并且根据情况编写可移植代码。这里,我们将描述如何在 MiniGUI 应用程序中编写可移植代码。

4.1 理解并使用 MiniGUI 的 Endian 读写函数
    为了解决上述的第一个问题,MiniGUI 提供了若干 Endian 相关的读写函数。这些函数可以划分为如下两类:

    用来交换字节序的函数。包括ArchSwapLE16、ArchSwapBE16 等。
    用来读写标准I/O 流的函数。包括MGUI_ReadLE16、MGUI_ReadBE16 等。

    前一类用来将某个 16位、32 位或者 64 位整数从某个特定的字节序转换为系统私有(native)字节序。举例如下:
    int fd, len_header;

...

    if (read (fd, &len_header, sizeof (int)) == -1)
        goto error;
#if MGUI_BYTEORDER == MGUI_BIG_ENDIAN
    len_header = ArchSwap32 (len_header);        // 如果是 Big Endian 系统,则转换字节序
#endif
...

    在上面的程序段中,首先通过 read 系统调用从指定的文件描述符中读取一个整数值到 len_header 变量中。该文件中保存的整数值是 Little Endian 的,因此如果在 Big Endian 系统上使用这个整数值,就必须进行字节顺序交换。这里可以使用 ArchSwapLE32,将 Little Endian 的 32 位整数值转换为系统私有的字节序。也可以如上述程序段那样,只对 Big Endian 系统进行字节序转换,这时,只要利用 ArchSwap32 函数即可。

    MiniGUI 提供的用来转换字节序的函数(或者宏)如下:

ArchSwapLE16(X) 将指定的以 Little Endian 字节序存放的 16 位整数值转换为系统私有整数值。如果系统本身是 Little Endian 系统,则该函数不作任何工作,直接返回 X;如果系统本身是 Big Endian 系统,则调用 ArchSwap16 函数交换字节序。
ArchSwapLE32(X) 将指定的以 Little Endian 字节序存放的 32 位整数值转换为系统私有整数值。如果系统本身是 Little Endian 系统,则该函数不作任何工作,直接返回 X;如果系统本身是 Big Endian 系统,则调用 ArchSwap32 函数交换字节序。
ArchSwapBE16(X) 将指定的以 Big Endian 字节序存放的 16 位整数值转换为系统私有整数值。如果系统本身是 Big Endian 系统,则该函数不作任何工作,直接返回 X;如果系统本身是 Little Endian 系统,则调用 ArchSwap16 函数交换字节序。
ArchSwapBE32(X) 将指定的以 Big Endian 字节序存放的 32 位整数值转换为系统私有整数值。如果系统本身是 Big Endian 系统,则该函数不作任何工作,直接返回 X;如果系统本身是 Little Endian 系统,则调用 ArchSwap32 函数交换字节序。

    MiniGUI 提供的第二类函数用来从标准 I/O 的文件对象中读写 Endian 整数值。如果要读取的文件是以 Little Endian 字节序存放的,则可以使用 MGUI_ReadLE16 和MGUI_ReadLE32 等函数读取整数值,这些函数将把读入的整数值转换为系统私有字节序,反之使用MGUI_ReadBE16 和MGUI_ReadBE32 函数。如果要写入的文件是以 Little Endian 字节序存放的,则可以使用 MGUI_WriteLE16 和MGUI_WriteLE32 等函数读取整数值,这些函数将把要写入的整数值从系统私有字节序转换为 Little Endian 字节序,然后写入文件,反之使用MGUI_WriteBE16 和MGUI_WriteBE32 函数。下面的代码段说明了上述函数的用法:
    FILE* out;
    int ount;
...
    MGUI_WriteLE32 (out, count);  // 以 Little Endian 字节序保存 count 到文件中。
...


4.2 利用条件编译编写可移植代码
    在涉及到可移植性问题的时候,有时我们能够方便地通过 4.1 中描述的方法进行函数封装,从而提供具有良好移植性的代码,但有时我们无法通过函数封装的方法提供可移植性代码。这时,恐怕只能使用条件编译了。下面的代码说明了如何使用条件编译的方法确保程序正常工作(该代码来自 MiniGUI src/kernel/sharedres.c):
/* 如果系统不支持共享内存,则定义 _USE_MMAP
#undef  _USE_MMAP
/* #define _USE_MMAP 1 */

void *LoadSharedResource (void)
{
#ifndef _USE_MMAP
    key_t shm_key;
    void *memptr;
    int shmid;
#endif

    /* 装载共享资源 */
    ...

#ifndef _USE_MMAP /* 获取共享内存对象 */
    if ((shm_key = get_shm_key ()) == -1) {
        goto error;
    }
    shmid = shmget (shm_key, mgSizeRes, SHM_PARAM | IPC_CREAT | IPC_EXCL);
    if (shmid == -1) {
        goto error;
    }

    // Attach to the share memory.
    memptr = shmat (shmid, 0, 0);
    if (memptr == (char*)-1)
        goto error;
    else {
        memcpy (memptr, mgSharedRes, mgSizeRes);
        free (mgSharedRes);
    }

    if (shmctl (shmid, IPC_RMID, NULL) < 0)
        goto error;
#endif

    /* 打开文件 */
    if ((lockfd = open (LOCKFILE, O_WRONLY | O_CREAT | O_TRUNC, 0644)) == -1)
        goto error;

#ifdef _USE_MMAP
    /* 如果使用 mmap,就将共享资源写入文件 */
    if (write (lockfd, mgSharedRes, mgSizeRes) < mgSizeRes)
        goto error;
    else
    {
        free(mgSharedRes);
        mgSharedRes = mmap( 0, mgSizeRes, PROT_READ|PROT_WRITE, MAP_SHARED, lockfd, 0);
    }
#else
    /* 否则将共享内存对象 ID 写入文件 */
    if (write (lockfd, &shmid, sizeof (shmid)) < sizeof (shmid))
        goto error;
#endif

    close (lockfd);

#ifndef _USE_MMAP
    mgSharedRes = memptr;
    SHAREDRES_SHMID = shmid;
#endif
    SHAREDRES_SEMID = semid;

    return mgSharedRes;

error:
    perror ("LoadSharedResource");
    return NULL;
}

    上述程序段是 MiniGUI-Lite 服务器程序用来装载共享资源的。如果系统支持共享内存,则初始化共享内存对象,并将装载的共享资源关联到共享内存对象,然后将共享内存对象 ID 写入文件;如果系统不支持共享内存,则将初始化后的共享资源全部写入文件。在客户端,如果支持共享内存,则可以从文件中获得共享内存对象 ID,并直接关联到共享内存;如果不支持共享内存,则可以使用 mmap 系统调用,将文件映射到进程的地址空间。客户端的代码段如下:
void* AttachSharedResource (void)
{
#ifndef _USE_MMAP
    int shmid;
#endif
    int lockfd;
    void* memptr;

    if ((lockfd = open (LOCKFILE, O_RDONLY)) == -1)
        goto error;

#ifdef _USE_MMAP
    /* 使用 mmap 将共享资源映射到进程地址空间 */
    mgSizeRes = lseek (lockfd, 0, SEEK_END );
    memptr = mmap( 0, mgSizeRes, PROT_READ, MAP_SHARED, lockfd, 0);
#else
    /* 否则获取共享内存对象 ID,并关联该共享内存 */
    if (read (lockfd, &shmid, sizeof (shmid)) < sizeof (shmid))
        goto error;
    close (lockfd);

    memptr = shmat (shmid, 0, SHM_RDONLY);
#endif
    if (memptr == (char*)-1)
        goto error;
    return memptr;

error:
    perror ("AttachSharedResource");
    return NULL;
}

5 其他

5.1 读写配置文件
MiniGUI 的配置文件,即 /etc/MiniGUI.cfg 文件的格式,采用了类似 Windows INI 文件的格式。这种文件格式非常简单,如下所示:
[section-name1]
key-name1=key-value1
key-name2=key-value2

[section-name2]
key-name3=key-value3
key-name4=key-value4

    这种配置文件中的参数以 section 分组,然后用 key=value 的形式指定参数及其值。应用程序也可以利用这种配置文件格式保存一些配置信息,为此,MiniGUI 提供了如下三个函数(include/minigui.h):
int GUIAPI GetValueFromEtcFile (const char* pEtcFile, const char* pSection,const char* pKey, char* pValue, int iLen);
int GUIAPI GetIntValueFromEtcFile (const char* pEtcFile, const char* pSection,const char* pKey, int* value);
int GUIAPI SetValueToEtcFile (const char* pEtcFile, const char* pSection, const char* pKey, char* pValue);

这三个函数的用途如下:

    ·GetValueFromEtcFile:从指定的配置文件当中获取指定的键值,键值以字符串形式返回。
    ·GetIntValueFromEtcFile:从指定的配置文件当中获取指定的整数型键值。该函数将获得的字符串转换为整数值返回(采用strtol 函数转换)。
    ·SetValueToEtcFile:该函数将给定的键值保存到指定的配置文件当中,如果配置文件不存在,则将新建配置文件。如果给定的键已存在,则将覆盖旧值。

假定某个配置文件记录了一些应用程序信息,并具有如下格式:
[mginit]
nr=8
autostart=0

[app0]
path=../tools/
name=vcongui
layer=
tip=Virtual&console&on&MiniGUI
icon=res/konsole.gif

[app1]
path=../bomb/
name=bomb
layer=
tip=Game&of&Minesweaper
icon=res/kmines.gif

[app2]
path=../controlpanel/
name=controlpanel
layer=
tip=Control&Panel
icon=res/kcmx.gif

    其中的 [mginit] 段记录了应用程序个数(nr键),以及自动启动的应用程序索引(autostart键)。而 [appX] 段记录了每个应用程序的信息,包括该应用程序的路径、名称、图标等等。下面的代码演示了如何使用 MiniGU的配置文件函数获取这些信息(该代码段来自 mde 演示包中的 mginit 程序):
#define APP_INFO_FILE "mginit.rc"

static BOOL get_app_info (void)
{
    int i;
    APPITEM* item;

    /* 获取应用程序个数信息 */
    if (GetIntValueFromEtcFile (APP_INFO_FILE, "mginit", "nr", &app_info.nr_apps) != ETC_OK)
        return FALSE;

    if (app_info.nr_apps <= 0)
        return FALSE;

    /* 获取自动启动的应用程序索引 */
    GetIntValueFromEtcFile (APP_INFO_FILE, "mginit", "autostart", &app_info.autostart);

    if (app_info.autostart >= app_info.nr_apps || app_info.autostart < 0)
        app_info.autostart = 0;

    /* 分配应用程序信息结构 */
    if ((app_info.app_items = (APPITEM*)calloc (app_info.nr_apps, sizeof (APPITEM))) == NULL) {
        return FALSE;
    }

    /* 获取每个应用程序的路径、名称、图标等信息 */
    item = app_info.app_items;
    for (i = 0; i < app_info.nr_apps; i++, item++) {
        char section [10];

        sprintf (section, "app%d", i);
        if (GetValueFromEtcFile (APP_INFO_FILE, section, "path", item->path, PATH_MAX) != ETC_OK)
            goto error;

        if (GetValueFromEtcFile (APP_INFO_FILE, section, "name", item->name, NAME_MAX) != ETC_OK)
            goto error;

        if (GetValueFromEtcFile (APP_INFO_FILE, section, "layer", item->layer, LEN_LAYER_NAME) != ETC_OK)
            goto error;

        if (GetValueFromEtcFile (APP_INFO_FILE, section, "tip", item->tip, TIP_MAX) != ETC_OK)
            goto error;

        strsubchr (item->tip, '&', ' ');

        if (GetValueFromEtcFile (APP_INFO_FILE, section, "icon", item->bmp_path, PATH_MAX + NAME_MAX) != ETC_OK)
            goto error;

        if (LoadBitmap (HDC_SCREEN, &item->bmp, item->bmp_path) != ERR_BMP_OK)
            goto error;

        item->cdpath = TRUE;
    }
    return TRUE;

error:
    free_app_info ();
    return FALSE;
}

5.2 定点数运算
    通常在进行数学运算时,我们采用浮点数表示实数,并利用 头文件中所声明的函数进行浮点数运算。我们知道,浮点数运算是一种非常耗时的运算过程。为了减少因为浮点数运算而带来的额外 CPU 指令,在一些三维图形库当中,通常会采用定点数来表示实数,并利用定点数进行运算,这样,将大大提高三维图形的运算速度。MiniGUI 也提供了一些定点数运算函数,分为如下几类:

    整数、浮点数和定点数之间的转换。利用 itofix 和 fixtoi 函数可实现整数和定点数之间的相互转换;利用 ftofix 和 fixtof 函数可实现浮点数和定点数之间的转换。
定点数加、减、乘、除等基本运算。利用 fadd、fsub、fmul、fdiv、fsqrt等函数可实现定点数加、减、乘、除以及平方根运算。
    定点数的三角运算。利用 fcos、fsin、ftan、facos、fasin 等函数可求给定定点数的余弦、正弦、正切、反余弦、反正弦值。
    矩阵、向量等运算。矩阵、向量相关运算在三维图形中非常重要,限于篇幅,本文不会详细讲述这些运算,读者可参阅MiniGUI 的 include/fixedmath.h 头文件。

    下面的代码段演示了定点数的用法,该程序段根据给定的三个点(pts[0]、pts[1]、pts[2])画一个弧线,其中 pts[0] 作为圆心,pts[1] 是圆弧的起点,而 pts[2] 是圆弧终点和圆心连线上的一个点:
void draw_arc (HDC hdc, POINT* pts)
{
            int sx = pts [0].x, sy = pts [0].y;
            int dx = pts [1].x - sx, dy = pts [1].y - sy;
            int r = sqrt (dx * dx * 1.0 + dy * dy * 1.0);
            double cos_d = dx * 1.0 / r;
            fixed cos_f = ftofix (cos_d);
            fixed ang1 = facos (cos_f);
            int r2;
            fixed ang2;

            if (dy > 0) {
                ang1 = fsub (0, ang1);
            }

            dx = pts [2].x - sx;
            dy = pts [2].y - sy;
            r2 = sqrt (dx * dx * 1.0 + dy * dy * 1.0);
            cos_d = dx * 1.0 / r2;
            cos_f = ftofix (cos_d);
            ang2 = facos (cos_f);
            if (dy > 0) {
                ang2 = fsub (0, ang2);
            }

            Arc (hdc, sx, sy, r, ang1, ang2);
}

    上述程序的计算非常简单,步骤如下(该程序段来自 mde 演示程序包中的 painter/painter.c 程序):

    根据 pts[0] 和 pts[1] 计算圆弧的半径,然后计算圆弧的起始偏角,即 ang1,使用了ftofix 函数和 facos 函数。
    计算 pts[2] 点和圆心连线的夹角,即 ang2,使用了 ftofix 和 facos 函数。
调用 Arc 函数绘制圆弧。

6 小结
    本文讲述了 MiniGUI 为应用程序提供的一些非 GUI/GDI 的接口。这些接口中,某些是为了解决和操作系统的交互而设计的,以便 MiniGUI 应用程序能够更好地与操作系统提供的机制融合在一起;而某些提供了对 UNIX Domain Socket 良好封装的接口,可帮助应用程序方便进行进程间通讯或者扩展其功能;其他接口则专注于嵌入式系统的特殊性,为应用程序提供了可移植的文件 I/O 封装代码。在这些接口的帮助下,嵌入式系统开发人员可以编写功能强大而灵活的应用程序。

[目录]


MiniGUI 和其他嵌入式 Linux 上的图形及图形用户界面系统

1 Linux 图形领域的基础设施
    本小节首先向读者描述 Linux 图形领域中常见的基础设施。之所以称为基础设施,是因为这些系统(或者函数库),一般作为其他高级图形或者图形应用程序的基本函数库。这些系统(或者函数库)包括:X Window、SVGALib、FrameBuffer 等等。

1.1 X Window
    提起 Linux 上的图形,许多人首先想到的是 X Window。这一系统是目前类 UNIX 系统中处于控制地位的桌面图形系统。无疑,X Window 作为一个图形环境是成功的,它上面运行着包括 CAD建模工具和办公套件在内的大量应用程序。但必须看到的是,由于 X Window 在体系接口上的原因,限制了其对游戏、多媒体的支持能力。用户在 X Window 上运行 VCD 播放器,或者运行一些大型的三维游戏时,经常会发现同样的硬件配置,却不能获得和 Windows 操作系统一样的图形效果――即使使用了加速的 X Server,其效果也不能令人满意。另外,大型的应用程序(比如 Mozilla 浏览器)在 X Window 上运行时的响应能力,也相当不能令人满意。当然,这里有 Linux 内核在进程调度上的问题,也有 X Window 的原因。

    X Window 为了满足对游戏、多媒体等应用对图形加速能力的要求,提供了 DGA(直接图形访问)扩展,通过该扩展,应用程序可以在全屏模式下直接访问显示卡的帧缓冲区,并能够提供对某些加速功能的支持。

    Tiny-X是XServer在嵌入式系统的小巧实现,它由Xfree86 Core Team 的Keith Packard开发。它的目标是运行于小内存系统环境。典型的运行于X86 CPU 上的Tiny-X Server 尺寸接近(小于)1MB。

1.2 SVGALib
    SVGALib 是 Linux 系统中最早出现的非 X 图形支持库。这个库从最初对标准 VGA 兼容芯片的支持开始,一直发展到对老式 SVGA 芯片的支持以及对现今流行的高级视频芯片的支持。它为用户提供了在控制台上进行图形编程的接口,使用户可以在 PC 兼容系统上方便地获得图形支持。但该系统有如下不足:

    接口杂乱。SVGALib 从最初的 vgalib 发展而来,保留了老系统的许多接口,而这些接口却不能良好地迎合新显示芯片的图形能力。
    未能较好地隐藏硬件细节。许多操作,不能自动使用显示芯片的加速能力支持。
可移植性差。SVGALib 目前只能运行在 x86 平台上,对其他平台的支持能力较差(Alpha 平台除外)。
    发展缓慢,有被其他图形库取代的可能。SVGALib 作为一个老的图形支持库,目前的应用范围越来越小,尤其在 Linux 内核增加了 FrameBuffer 驱动支持之后,有逐渐被其他图形库替代的迹象。
    对应用的支持能力较差。SVAGLib 作为一个图形库,对高级图形功能的支持,比如直线和曲线等等,却不能令人满意。尽管 SVGALib 有许多缺点,但 SVGALib 经常被其他图形库用来初始化特定芯片的显示模式,并获得映射到进程地址空间的线性显示内存首地址(即帧缓冲区),而其他的接口却很少用到。另外,SVGALib 中所包含的诸如键盘、鼠标和游戏杆的接口,也很少被其他应用程序所使用。

    因此,SVGALib 的使用越来越少,笔者也不建议用户使用这个图形库。当然,如果用户的显示卡只支持标准 VGA 模式,则 SVGALib 还是比较好的选择。

1.3 FrameBuffer
    FrameBuffer 是出现在 2.2.xx 内核当中的一种驱动程序接口。这种接口将显示设备抽象为帧缓冲区。用户可以将它看成是显示内存的一个映像,将其映射到进程地址空间之后,就可以直接进行读写操作,而写操作可以立即反应在屏幕上。该驱动程序的设备文件一般是 /dev/fb0、/dev/fb1 等等。比如,假设现在的显示模式是 1024x768-8 位色,则可以通过如下的命令清空屏幕:

$ dd if=/dev/zero of=/dev/fb0 bs=1024 count=768

    在应用程序中,一般通过将 FrameBuffer 设备映射到进程地址空间的方式使用,比如下面的程序就打开 /dev/fb0 设备,并通过 mmap 系统调用进行地址映射,随后用 memset 将屏幕清空(这里假设显示模式是 1024x768-8 位色模式,线性内存模式):

int fb;
unsigned char* fb_mem;

fb = open ("/dev/fb0", O_RDWR);
fb_mem = mmap (NULL, 1024*768, PROT_READ|PROT_WRITE,MAP_SHARED,fb,0);

memset (fb_mem, 0, 1024*768);


    FrameBuffer 设备还提供了若干 ioctl 命令,通过这些命令,可以获得显示设备的一些固定信息(比如显示内存大小)、与显示模式相关的可变信息(比如分辨率、象素结构、每扫描线的字节宽度),以及伪彩色模式下的调色板信息等等。

    通过 FrameBuffer 设备,还可以获得当前内核所支持的加速显示卡的类型(通过固定信息得到),这种类型通常是和特定显示芯片相关的。比如目前最新的内核(2.4.9)中,就包含有对 S3、Matrox、nVidia、3Dfx 等等流行显示芯片的加速支持。在获得了加速芯片类型之后,应用程序就可以将 PCI 设备的内存I/O(memio)映射到进程的地址空间。这些 memio 一般是用来控制显示卡的寄存器,通过对这些寄存器的操作,应用程序就可以控制特定显卡的加速功能。

    PCI 设备可以将自己的控制寄存器映射到物理内存空间,而后,对这些控制寄存器的访问,给变成了对物理内存的访问。因此,这些寄存器又被称为"memio"。一旦被映射到物理内存,Linux 的普通进程就可以通过 mmap 将这些内存 I/O 映射到进程地址空间,这样就可以直接访问这些寄存器了。

    当然,因为不同的显示芯片具有不同的加速能力,对memio 的使用和定义也各自不同,这时,就需要针对加速芯片的不同类型来编写实现不同的加速功能。比如大多数芯片都提供了对矩形填充的硬件加速支持,但不同的芯片实现方式不同,这时,就需要针对不同的芯片类型编写不同的用来完成填充矩形的函数。

    说到这里,读者可能已经意识到 FrameBuffer 只是一个提供显示内存和显示芯片寄存器从物理内存映射到进程地址空间中的设备。所以,对于应用程序而言,如果希望在 FrameBuffer 之上进行图形编程,还需要完成其他许多工作。举个例子来讲,FrameBuffer 就像一张画布,使用什么样子的画笔,如何画画,还需要你自己动手完成。

1.4 LibGGI
    LibGGI 试图建立一个一般性的图形接口,而这个抽象接口连同相关的输入(鼠标、键盘、游戏杆等)抽象接口一起,可以方便地运行在 X Window、SVGALib、FrameBuffer 等等之上。建立在 LibGGI 之上的应用程序,不经重新编译,就可以在上述这些底层图形接口上运行。但不知何故,LibGGI 的发展几乎停滞。

2 Linux 图形领域的高级函数库

2.1 Xlib 及其他相关函数库
    在 X Window 系统中进行图形编程时,可以选择直接使用 Xlib。Xlib 实际是对底层 X 协议的封装,可通过该函数库进行一般的图形输出。如果你的 X Server 支持 DGA,则可以通过 DGA 扩展直接访问显示设备,从而获得加速支持。对一般用户而言,由于 Xlib 的接口太原始而且复杂,因此一般的图形程序选择其他高级一些的图形库作为基础。比如,GTK、QT 等等。这两个函数同时还是一些高级的图形用户界面支持函数库。由于种种原因,GTK、QT 等函数库存在有庞大、占用系统资源多的问题,不太适合在嵌入式系统中使用。这时,你可以选择使用 FLTK,这是一个轻量级的图形函数库,但它的主要功能集中在用户界面上,提供了较为丰富的控件集。

2.2 SDL
    SDL(Simple DirectMedia Layer)是一个跨平台的多媒体游戏支持库。其中包含了对图形、声音、游戏杆、线程等等的支持,目前可以运行在许多平台上,其中包括 X Window、X Window with DGA、Linux FrameBuffer 控制台、Linux SVGALib,以及Windows DirectX、BeOS 等等。

    因为 SDL 专门为游戏和多媒体应用而设计开发,所以它对图形的支持非常优秀,尤其是高级图形能力,比如 Alpha 混和、透明处理、YUV 覆盖、Gamma 校正等等。而且在 SDL 环境中能够非常方便地加载支持 OpenGL 的 Mesa 库,从而提供对二维和三维图形的支持。

    可以说,SDL 是编写跨平台游戏和多媒体应用的最佳平台,也的确得到了广泛应用。相关信息,可参阅 http://www.libsdl.org。

2.3 Allegro
    Allegro 是一个专门为 x86 平台设计的游戏图形库。最初的 Allegro 运行在 DOS 环境下,而目前可运行在 Linux FrameBuffe 控制台、Linux SVGALib、X Window 等系统上。Allegro 提供了一些丰富的图形功能,包括矩形填充和样条曲线生成等等,而且具有较好的三维图形显示能力。由于 Allegro 的许多关键代码是采用汇编编写的,所以该函数库具有运行速度快、资源占用少的特点。然而,Allegro 也存在如下缺点:

    ·对线程的支持较差。Allegro 的许多函数是非线程安全的,不能同时在两个以上的线程中使用。
    ·硬件加速能力的支持不足,在设计上没有为硬件加速提供接口。

    有关 Allegro 的进一步信息,可参阅http://www.allegro.cc/。

2.4 Mesa3D
    Mesa3D 是一个兼容 OpenGL 规范的开放源码函数库,是目前 Linux 上提供专业三维图形支持的惟一选择。Mesa3D 同时也是一个跨平台的函数库,能够运行在 X Window、X Window with DGA、BeOS、Linux SVGALib 等平台上。

    有关 Mesa3D 的进一步信息,可参阅 http://www.mesa3d.org/。

2.5 DirectFB
    DirectFB 是专注于 Linux FrameBuffer 加速的一个图形库,并试图建立一个兼容 GTK 的嵌入式 GUI 系统。它以可装载函数库的形势提供对加速 FrameBuffer 驱动程序的支持。目前,该函数库正在开发之中(最新版本 0.9.97),详情可见 http://www.directfb.org/。

3 面向嵌入式Linux 系统的图形用户界面

3.1 MicoroWindows/NanoX
    MicroWindows(http://microwindows.censoft.com)是一个开放源码的项目,目前由美国 Century Software 公司主持开发。该项目的开发一度非常活跃,国内也有人参与了其中的开发,并编写了 GB2312 等字符集的支持。但在 Qt/Embedded 发布以来,该项目变得不太活跃,并长时间停留在 0.89Pre7 版本。可以说,以开放源码形势发展的 MicroWindows 项目,基本停滞。

    MicroWindows 是一个基于典型客户/服务器体系结构的 GUI 系统,基本分为三层。最底层是面向图形输出和键盘、鼠标或触摸屏的驱动程序;中间层提供底层硬件的抽象接口,并进行窗口管理;最高层分别提供兼容于 X Window 和 Windows CE(Win32 子集)的 API。

    该项目的主要特色在于提供了类似 X 的客户/服务器体系结构,并提供了相对完善的图形功能,包括一些高级的功能,比如 Alpha 混合,三维支持,TrueType 字体支持等。但需要注意的是,MicroWindows 的图形引擎存在许多问题,可以归纳如下:

无任何硬件加速能力。
    图形引擎中存在许多低效算法,同时未经任何优化。比如在直线或者圆弧绘图函数中,存在低效的逐点判断剪切的问题。
    代码质量较差。由于该项目缺少一个强有力的核心代码维护人员,因此代码质量参差不齐,影响整体系统稳定性。这也是 MicroWindows 长时间停留在 0.89Pre7 版本上的原因。

MicroWindows 采用 MPL 条款发布(该条款基本类似 LGPL 条款)。

3.2 OpenGUI
    OpenGUI(http://www.tutok.sk/fastgl/)在 Linux 系统上存在已经很长时间了。最初的名字叫 FastGL,只支持 256 色的线性显存模式,但目前也支持其他显示模式,并且支持多种操作系统平台,比如 MS-DOS、QNX 和 Linux 等等,不过目前只支持 x86 硬件平台。OpenGUI 也分为三层。最低层是由汇编编写的快速图形引擎;中间层提供了图形绘制 API,包括线条、矩形、圆弧等,并且兼容于 Borland 的 BGI API。第三层用 C++ 编写,提供了完整的 GUI 对象集。

    OpenGUI 采用 LGPL 条款发布。OpenGUI 比较适合于基于 x86 平台的实时系统,可移植性稍差,目前的发展也基本停滞。

3.3 Qt/Embedded
    Qt/Embedded是著名的 Qt 库开发商 TrollTech(http://www.trolltech.com/)发布的面向嵌入式系统的 Qt 版本。因为 Qt 是 KDE 等项目使用的 GUI 支持库,所以有许多基于 Qt 的 X Window 程序可以非常方便地移植到 Qt/Embedded 版本上。因此,自从 Qt/Embedded 以 GPL 条款形势发布以来,就有大量的嵌入式 Linux 开发商转到了 Qt/Embedded 系统上。比如韩国的 Mizi 公司,台湾省的某些嵌入式 Linux 应用开发商等等。

    不过,在笔者看来,Qt/Embedded 还有一些问题值得开发者注意:

    目前,该系统采用两种条款发布,其中包括 GPL 条款。对函数库使用 GPL 条款,意味着其上的应用需要遵循 GPL 条款。当然了,如果要开发商业程序,TrollTech 也允许你采用另外一个授权条款,这时,就必须向 TrollTech 交纳授权费用了。
    Qt/Embedded 是一个 C++ 函数库,尽管 Qt/Embedded 声称可以裁剪到最少 630K,但这时的 Qt/Embedded 库已经基本上失去了使用价值。低的程序效率、大的资源消耗也对运行 Qt/Embedded 的硬件提出了更高的要求。
Qt/Embedded 库目前主要针对手持式信息终端,因为对硬件加速支持的匮乏,很难应用到对图形速度、功能和效率要求较高的嵌入式系统当中,比如机顶盒、游戏终端等等。
    Qt/Embedded 提供的控件集风格沿用了 PC 风格,并不太适合许多手持设备的操作要求。
    Qt/Embedded 的结构过于复杂,很难进行底层的扩充、定制和移植,尤其是那个用来实现 signal/slot 机制的著名的 moc 文件。

    因为上述这些原因,目前所见到的 Qt/Embedded 的运行环境,几乎是清一色基于 StrongARM 的 iPAQ。

3.4 MiniGUI
    MiniGUI(http://www.minigui.org)是由笔者主持,并由许多自由软件开发人员支持的一个自由软件项目(遵循 LGPL 条款发布),其目标是为基于 Linux 的实时嵌入式系统提供一个轻量级的图形用户界面支持系统。该项目自 1998 年底开始到现在,已历经 3 年多的开发过程。到目前为止,已经非常成熟和稳定。目前,我们已经正式发布了稳定版本 1.0.9,并且开始了新版本系列的开发,即 MiniGUI Version 1.1.x,该系列的正式版也即将发布。

    在 MiniGUI 几年的发展过程中,有许多值得一提的技术创新点,正是由于这些技术上的创新,才使得 MiniGUI 更加适合实时嵌入式系统;而且 MiniGUI 的灵活性非常好,可以应用在包括手持设备、机顶盒、游戏终端等等在内的各种高端或者低端的嵌入式系统当中。这些技术创新包括:

    图形抽象层。图形抽象层对顶层 API 基本没有影响,但大大方便了 MiniGUI 应用程序的移植、调试等工作。目前包含三个图形引擎,SVGALib、LibGGI 以及直接基于 Linux FrameBuffer 的 Native Engine,利用 LibGGI 时,可在 X Window 上运行 MiniGUI 应用程序,并可非常方便地进行调试。与图形抽象层相关的还有输入事件的抽象层。MiniGUI 现在已经被证明能够在基于 ARM、MIPS、StrongARM 以及 PowerPC 等的嵌入式系统上流畅运行。
多字体和多字符集支持。这部分通过设备上下文(DC)的逻辑字体(LOGFONT)实现,不管是字体类型还是字符集,都可以非常方便地进行扩充。应用程序在启动时,可切换系统字符集,比如 GB、BIG5、EUCKR、UJIS。利用 DrawText 等函数时,可通过指定字体而获得其他字符集支持。对于一个窗口来说,同时显示不同语种的文字是可能的。MiniGUI 的这种字符集支持不同于传统通过 UNICODE 实现的多字符集支持,这种实现更加适合于嵌入式系统。
两个不同架构的版本。最初的 MiniGUI 运行在 PThread 库之上,这个版本适合于功能单一的嵌入式系统,但存在系统健壮性不够的缺点。在 0.9.98 版本中,我们引入了 MiniGUI-Lite 版本,这个版本在提高系统健壮性的同时,通过一系列创新途径,避免了传统 C/S 结构的弱点,为功能复杂的嵌入式系统提供了一个高效、稳定的 GUI 系统。

    在 MiniGUI 1.1.0 版本的开发中,我们参照 SDL 和 Allegro 的图形部分,重新设计了图形抽象层,并增强了图形功能,同时增强了 MiniGUI-Lite 版本的某些特性。这些特性包括:

    MiniGUI-Lite 支持层的概念。同一层可容纳多个能够同时显示的客户程序,并平铺在屏幕上显示。
新的 GAL 能够支持硬件加速能力,并能够充分使用显示内存;新 GAL 之上的新 GDI 接口得到进一步增强。新的 GDI 接口可以支持 Alpha 混和、透明位块传输、光栅操作、YUV覆盖、Gamma 校正,以及高级图形功能(椭圆、多边形、样条曲线)等等。

    MiniGUI 新版本在图形方面的增强和提高,将大大扩展它的应用领域,希望能够对嵌入式 Linux 上的多媒体应用、游戏开发提供支持。

    纵观嵌入式 Linux 系统上的各种图形系统方案,我们发现,许多图形系统(如 Qt/Embedded 和 MicoroWindows),只注重手持设备上的需求,却不太注重其他应用领域的需求,而其他许多需要图形支持的嵌入式 Linux 系统却需要许多独特的、高级的图形功能,而不仅仅是图形用户界面。为此,在接下来的开发中,我们还将在如下领域继续开发 MiniGUI:

    ·提供运行在 MiniGUI 上的 JAVA 虚拟机 AWT 组件的实现。
    ·提供 MiniGUI 上的 OpenGL 实现。
    ·提供类 QT 控件集的 C++ 封装。
    ·提供窗口/控件风格主题支持。
    ·在 MiniGUI-Lite 当中增加对矢量字体的支持。

4 小结
    综上所述,笔者认为在嵌入式 Linux 图形领域,还有许多有待开发人员仔细研究和解决的问题。MiniGUI 的新的发展,也正源于对这些需求的认识之上。我们也衷心希望能够有更多的自由软件开发人员加盟 MiniGUI 的开发,一同开发新的嵌入式 Linux 的图形系统。

[目录]


Lite的新改进

    MiniGUI从0.98开始推出Lite版本。Lite版本是MiniGUI迈向嵌入式应用重要的一步。在Lite版本中,我们使用了自己设计的引擎,抛弃了pthread库,从而使得MiniGUI能够轻装上阵,更稳定,更高效率,也更符合嵌入式系统应用。本文介绍了MiniGUI Lite版本的基于Unix IPC实现的多进程机制。并详细介绍了一些实现细节。

1 引言:为什么要开发Lite版本

    现在,大多数UNIX系统采用X 窗口系统作为图形用户界面,MS Windows 则采用 Microsoft公司自己设计的GUI系统。这两种GUI系统也代表着目前通用GUI系统的两种实现。比如,著名的自由软件MicroWindows就同时实现了类似于MS Windows的MicroWindows API 和类似于X Window的NanoX API。

    MiniGUI 原来就采用了类似于MS Windows的体系结构,并且建立了基于线程的消息传递和窗口管理机制。然而,它是基于POSIX 线程的,这种实现提供最大程度上的数据共享,但也同时造成MiniGUI体系结构上的脆弱。如果某个线程因为非法的数据访问而终止运行,则整个系统都将受到影响。

    另一种方法是采用UNIX进程间通信机制建立窗口系统,即类似 X Window 的客户/服务器体系。但是这种体系结构也有它的先天不足,主要是通常的 IPC 机制无法提供高效的数据复制,大量的 CPU 资源用于各进程之间复制数据。在 PDA 等设备中,这种 CPU 资源的浪费将最终导致系统性能的降低以及设备耗电量的增加。

    为了解决以上各种问题,同时也为了让 MiniGUI更加适合于嵌入式系统,我们开发了MiniGUI Lite 版本。

2 Lite版本简介

    在MiniGUI Lite 版本中,我们可以同时运行多个 MiniGUI 应用程序。首先我们启动一个服务器程序 mginit,然后我们可以从中启动其他做为客户运行的 MiniGUI 应用程序。如果因为某种原因客户终止,服务器可以继续运行。mginit程序建立了一个虚拟控制台窗口。我们可以从这个虚拟控制台的命令行启动其他的程序,甚至可以通过 gdb 调试这些程序。 这大大方便了MiniGUI应用程序的调试。

    MiniGUI-Lite 区别于 MiniGUI 原有版本的最大不同在于我们可以在 MiniGUI-Lite 程序中创建多个窗口,但不能启动新的线程建立窗口。除此之外,其他几乎所有的 API 都和 MiniGUI 原有版本是兼容的。因此,从 MiniGUI 原有版本向 MiniGUI-Lite 版本的移植是非常简单的。象mglite-exec 包中的程序,其中所有的程序均来自 miniguiexec 包,而每个源文件的改动不超过 5 行。

3 Lite版本的设计

    设计之初,我们确定MiniGUI Lite 版本的开发目的:

    ·保持与原先 MiniGUI 版本在源代码级 98% 以上的兼容。
    ·不再使用 LinuxThreads。
    ·可以同时运行多个基于 MiniGUI Lite 的应用程序,即多个进程,并且提供前后台进程的切换。

    显然,要满足这三个设计目的,如果采用传统的 C/S 结构对现有 MiniGUI 进行改造,应该不难实现。但传统 C/S 结构的缺陷却无法避免。经过对 PDA 等嵌入式系统的分析,我们发现,某些 PDA 产品具有运行多个任务的能力,但同一时刻在屏幕上进行绘制的程序,一般不会超过两个。因此,只要确保将这两个进程的绘制相互隔离,就不需要采用复杂的 C/S 结构处理多个进程窗口之间的互相剪切。在这种产品中,如果采用基于传统 C/S 结构的多窗口系统,实际是一种浪费。

因此,我们对 MiniGUI-Lite 版本进行了如下简化设计:

    每个进程维护自己的主窗口 Z 序,同一进程创建的主窗口之间互相剪切。也就是说,除了只有一个线程,只有一个消息循环之外,一个进程与原有的 MiniGUI 版本之间没有任何区别。每个进程在进行屏幕绘制时,不需要考虑其他进程。
    建立一个简单的客户/服务器体系,但确保最小化进程间的数据复制功能。因此,在服务器和客户之间传递的数据仅限于输入设备的输入数据,以及客户和服务器之间的某些请求和响应数据。
有一个服务器进程(mginit),它负责初始化一些输入设备,并且通过 UNIX Domain 套接字将输入设备的消息发送到前台的 MiniGUI Lite 客户进程。
    服务器和客户被分别限定在屏幕的某两个不相交矩形内进行绘制,同一时刻,只能有一个客户及服务器进行屏幕绘制。其他客户可继续运行,但屏幕输入被屏蔽。服务器可以利用 API 接口将某个客户切换到前台。同时,服务器和客户之间采用信号和 System V 信号量进行同步。
    服务器还采用 System V IPC 机制提供一些资源的共享,包括位图、图标、鼠标、字体等等,以便减少实际内存的消耗。
4 Lite版本的一些实现细节

4.1 系统初始化
    应用程序的入口点为main()函数,而MiniGUI应用程序的入口点为MiniGUIMain,在这两个入口点之间,是MiniGUI的初始化部分和结束部分。

    在系统初始化时,MiniGUI区分两种情况:服务器(Server)和客户(Client)。针对这两种不同的情况,随后的各项操作均有不同的处理,这主要依据全局变量mgServer。由于仅仅根据名称判断是否为服务器,所以服务器的名字只能是"mginit"。 InitGUI()是对MiniGUI进行初始化的函数,它主要负责:

获取有关终端的信息。
初始化图形抽象层。
如果是服务器,则装入共享资源,若为客户则与共享资源建立连接。
建立与窗口活动有关的运行环境。
如果为服务器,则初始化事件驱动抽象层(IAL),如果为客户,则打开与服务器事件驱动器的通道。
如果为服务器,则设定空闲处理为IdleHandler4Server,如果为客户,则设定空闲处理为IdleHandle4Client。
流程如图 2(为突出重点,我们忽略了一些细节):

4.2 共享资源初始化
    共享资源是客户服务器模型中的重要元素,它由服务器负责创建和释放,而提供所有客户程序共享的数据资源。它的初始化过程由图 3所示的调用流完成。

    如果是服务器,则初始化此结构,src/kernel/sharedres.c/LoadSharedResource()负责完成这一任务

    对于客户,则只需要与此结构进行连接即可,它在src/kernel/sharedres.c/AttachSharedResource()实现

4.3 服务器客户通信连接初始化
    在客户服务器模型的讨论中,我们还将详细的讨论服务器客户的通信机制,这里只给出初始化的调用关系。

4.4 多进程模型
    Lite版本是支持客户服务器(C/S)方式的多进程系统,在运行过程中有且仅有一个服务器程序在运行,它的全局变量mgServer被设为TRUE,其余的MiniGUI应用程序为客户,mgServer变量被设为FALSE。各个应用程序分别运行于各自不同的进程空间

    目前的程序结构使每个加载的进程拥有一个自已的桌面模型及其消息队列,进程间的通信依靠以下所提到的进程通信模型来完成。

4.5 进程通信模型
    这里我们所指的进程通信包括通过共享内存实现的数据交换和通过套接字实现的客户服务器通信模型。

    服务器负责装入共享资源,其中包括系统图标、位图、字体等,客户则通过AttachSharedResource()获取指向共享资源的指针,初始化一块共享内存及与使用已有共享内存的方法在前面的描述中已提到,在此不再赘述。

4.6 各进程之间的同步
    这里所指的进程同步主要是指各进程绘制的同步,显然,同时不可能有两个进程向屏幕绘制。传统的GUI实现大多是只有一个进程负责绘制,而在我们Lite版本中,各进程负责自己的绘制。同时,我们的Lite 版本还支持虚屏切换,当我们切换出去的时候,谁也不能够向屏幕绘制。

    Lite 版本利用Unix 信号解决了绘制同步问题。系统定义了两个信号:SIG_SETSCR 和 SIG_DNTDRAW,它们其实是重定义了的信号SIGUNUSED和 SIGSTKFLT。每个进程都定义了两个变量dont_draw和cant_draw。

    服务器利用SIG_SETSCR和SIG_DNTDRAW来控制各客户程序谁有权对屏幕绘制,而不是自己全权代理。这也使得进程间通信量大大减少:当服务器希望一个客户程序不要向屏幕绘制时,就向它发送SIG_DNTDRAW信号,当让其绘制时,则发送SIG_SETSCR。从而实现了各进程间的屏幕绘制同步。

    当一个客户收到SIG_DNTDRAW时,将自己的变量dont_daw设置为ture,收到SIG_SETSCR时,则将dont_draw变量设置为false。另一个变量cant_draw则是给客户自己用的,比如,做剪切时,当它的剪切域为空集时,又比如,当进行虚屏切换时,当前的进程将自己的cant_draw变量设置为true。

    另外,如果一个客户正在绘图,我们只有等它画完后才能让其他进程得到这一权利。我们不需要知道谁在绘图,但我们要等到这一过程结束。Lite版本利用信号量机制解决了这一问题。在共享内存里保存着一个变量shmid,各进程利用它来实现各自的锁机制。这种机制有点类似于文件锁,不过要快许多。

    从而,利用信号量机制,Lite版本实现了多进程的绘制同步。服务器利用信号控制各客户,而各客户也充分合作。相关代码都在MiniGUI的系统库里实现。保证了系统的稳定运行。

5 总结语

    MiniGUI Lite版本试图在传统的基于线程的单体结构和C/S结构之间寻求一种效率和稳定性的折中,以便更加适合运行在PDA等小型嵌入式系统中。如今,MiniGUI Lite版本已经稳定地运行在一些PDA系统上,事实证明这种尝试是成功的。


[目录]


安装手册

MiniGUI 简易安装手册
魏永明 (2001/08/09)

1. 安装前的准备
1) 选择图形引擎
    如果您的 Linux 内核支持 FrameBuffer, 则可以使用内建于 MiniGUI 的图形引擎 -- 即 "私有引擎". 这样, 就没有必要安装下面提到的其它图形函数库作为 MiniGUI 的图形引擎了. 而且, Native 引擎是唯一支持 MiniGUI-Lite 的引擎, 如果您要将 MiniGUI 配置为 MiniGUI-Lite, 也没有必要安装其它的图形库作为引擎.

    Native 引擎既可以支持 MiniGUI-Threads, 也可以支持 MiniGUI-Lite. 需要注意的是, Native 引擎目前还不能提供对 "fbvga16" (标准 VGA 16 色模式) 的良好支持. 而且需要注意 Native 引擎目前只能运行在 Linux FrameBuffer 之上.

    如果您的 Linux 内核不支持 FrameBuffer, 则可以使用 SVGALib 作为图形引擎. SVGALib 是一种直接访问图形芯片的老的图形函数库. 因此, 如果使用 SVGALib, 则需要获得 root 权限才能让 MiniGUI 程序支持进行图形芯片的 I/O 操作.

    需要注意的是, 因为 SVGALib 是一种老的图形库, 所以不能对大部分流行的显示卡提供良好支持. 然而, 如果您的显示芯片是 VESA 兼容的, 则 SVGALib 可以通过 VESA BIOS 2.0 提供的接口良好运行. 许多显示芯片都是 VESA 兼容的, 但某些不是, 比如 Intel 的 i810 芯片组.

    你也可以使用 LibGGI 作为 MiniGUI 的图形引擎. LibGGI 是一种新的面向 Linux 的图形引擎, 它也可以稳定地运行在 Linux FrameBuffer 之上. 最主要的是, 运行在 LibGGI 之上的应用程序可以方便地运行在 X Window 之上, 而且不需要重新编译.

    注意, SVGALib 和 LibGGI 不能用来支持 MiniGUI-Lite.

2) 安装引擎
    如果您决定使用 Native 引擎, 则没有必要预先安装某个特定的图形库. Native 引擎已经内建于 MiniGUI 当中.

    如果要使用 SVGALib 作为图形引擎, 则需要安装经过修改的 "svgalib-1.4.0-hz" 软件包和 "vgagl4-0.1.0" 包. 请从如下 FTP 站点下载:

    ftp://ftp.minigui.net/pub/minigui/dep-libs

    或者从 HTTP 站点下载:

    http://www.minigui.org/cdownload.shtml

    并将上述两个软件包安装到您的系统当中. 安装过程将覆盖系统中老的 SVGALib, 但并不会影响您的系统.

    如果您使用 LibGGI 作为图形引擎, 请从 http://www.ggi-projects.org 下载最新的 GGI 源代码, 并安装之. 或者, 也可以从我们的 FTP 站点上下载:

    ftp://ftp.minigui.net/pub/minigui/dep-libs

    或者从 HTTP 站点下载:

    http://www.minigui.org/cdownload.shtml

3) 下载 MiniGUI
    在下载并安装好图形引擎之后, 请从我们的站点上下载如下 tar.gz 软件包并解开:

libminigui-1.0.xx.tar.gz: MiniGUI 函数库源代码, 其中包括 libminigui, libmywins, libmgext, 和 libvcongui.
minigui-res-1.0.xx.tar.gz: MiniGUI 所使用的资源, 包括图标, 位图和鼠标光标.
minigui-fonts-1.0.xx.tar.gz: MiniGUI 所使用的基本字体. 在我们的站点上, 还可以找到其它字体.
minigui-imetabs-1.0.xx.tar.gz: 中文 GB 输入法所使用的码表.
minigui-exec-1.0.xx.tar.gz: MiniGUI-Threads 的示例程序.
mglite-exec-1.0.xx.tar.gz: MiniGUI-Lite 的示例程序.

    注意, 如果你以前安装过 "minigui-res", "minigui-fonts", 或者 "minigui-imetabs" 的老版本, 而且站点上并没有提供新版本的话, 说明您可以继续沿用老的版本.

2 安装 MiniGUI 的资源文件
    我们首先要安装 MiniGUI 的资源文件. 请按照如下步骤:

1) 使用 "tar" 命令解开 "minigui-res-1.0.xx.tar.gz". 可使用如下命令:

           $ tar zxf minigui-res-1.0.xx.tar.gz

2) 改变到新建目录中, 然后以超级用户身份运行 "make" 命令:

           $ su -c make install

3) 使用相同的步骤安装 "minigui-fonts" 和 "minigui-imetabs" 软件包.

3 配置和编译 MiniGUI
    MiniGUI 使用了 "automake" 和 "autoconf" 接口, 因而 MiniGUI 的配置和编译非常容易:

1) 使用 "tar" 解开 "libminigui-1.0.xx.tar.gz" 到新的目录:

           $ tar zxf libminigui-1.0.xx.tar.gz

2) 改变到新目录, 然后运行 "./configure":

           $ ./configure

3) 运行下面的命令编译并安装 MiniGUI:

           $ make; su -c 'make install';

4) 默认情况下, MiniGUI 的函数库将安装在 `/usr/local/lib' 目录中. 您应该确保该目录已经列在 "/etc/ld.so.conf" 文件中. 并且在安装之后, 要运行下面的命令更新共享函数库系统的缓存:

            $ su -c ldconfig

5) 如果要控制您的 MiniGUI 提供那些功能, 则可以运行:

           $ ./configure --help

    查看完整的配置选项清单, 然后通过命令行开关打开或者关闭某些功能. 例如, 如果您不希望 MiniGUI 使用 LoadBitmap 函数装载 JPEG 图片, 则可以使用:

           $ ./configure --disable-jpgsupport

6) 注意, 某些 MiniGUI 特色依赖于其它函数库, 请确保已安装了这些函数库.

4 运行 MiniGUI 的演示程序
    "minigui-exec-1.0.xx.tar.gz" 和 "mglite-exec-1.0.xx.tar.gz" 中分别包含了 MiniGUI-Threads 和 MiniGUI-Lite 的演示程序. 如果将 MiniGUI 配置为 MiniGUI-Threads, 则请编译并运行 minigui-exec 软件包, 否则, 请编译并运行 mglite-exec 软件包.

运行之前, 应该解开并编译这些 tar.gz 包:

1) 使用 "tar" 命令将软件包解开到新的目录.

2) 依次运行 "./configure" 和 "make" 编译演示程序.

3) 运行 "make install" 将安装其中的一些应用程序到系统中. 注意这些程序将被安装到 "/usr/local/bin" 目录中, 而某些演示程序并不会安装到系统当中.

4) 尝试运行演示程序和应用程序. 例如, 如果将 MiniGUI 配置为 MiniGUI-Threads, 则可以进入 "minigui-exec-1.0.xx/amuze/" 运行 "./amuze".

5) 如果配置并安装了 MiniGUI-Lite, 则应该首先运行服务器, 然后从服务器当中运行其它演示程序. 编译 "mglite-exec" 将生成一个 "mginit" 程序, 该程序将提供一个运行于 MiniGUI-Lite 的虚拟控制台.

5. 安装及配置示例
    本示例假定用户使用的系统是 RedHat Linux 6.x 发行版, 使用 Linux 内核 2.2.xx 或者 2.4.xx, 用户的目标是运行 MiniGUI-Lite (使用 MiniGUI Version 1.0.00). 步骤如下:

1) 确保您的 PC 机显示卡是 VESA 兼容的. 大多数显示卡是 VESA 兼容的, 然而某些内嵌在主板上的显示卡可能不是 VESA 兼容的, 比如 Intel i810 系列. 如果显示卡是 VESA 兼容的, 就可以使用 Linux 内核中的 VESA FrameBuffer 驱动程序了.

2) 确保您的 Linux 内核包含了 FrameBuffer 支持, 并包含了 VESA FrameBuffer 驱动程序. RedHat Linux 6.x 发行版自带的内核中已经包含了该驱动程序. 如果使用自己编译的内核, 请检查您的内核配置.

3) 修改 /etc/lilo.conf 文件, 在您所使用的内核选项段中, 添加如下一行:

                vga=0x0317

    这样, Linux 内核在启动时将把显示模式设置为 1024x768x16bpp 模式. 如果您的显示器无法达到这种显示分辨率, 可考虑设置 vga=0x0314, 对应 800x600x16bpp 模式.

修改后的 /etc/lilo.conf 文件类似:

                boot=/dev/hda
                map=/boot/map
                install=/boot/boot.b
                prompt
                timeout=50
                linear
                default=linux

                image=/boot/vmlinuz-2.4.2
                    vga=0x0317          ; 这一行设置显示模式.
                    label=linux
                    read-only
                    root=/dev/hda6

                other=/dev/hda1
                    label=dos

4) 运行 lilo 命令, 使所作的修改生效, 并重新启动系统.

5) 如果一切正常, 将在 Linux 内核的引导过程中看到屏幕左上角出现可爱的 Linux 吉祥物 -- 企鹅, 并发现系统的显示模式发生变化.

6) 按照第 4 节所讲, 下载 libminigui-1.0.00.tar.gz, mglite-exec-1.0.02.tar.gz, 以及 minigui-res-1.0.02.tar.gz, minigui-fonts-1.0.00.tar.gz, minigui-imetabs-0.9.96.tar.gz 等软件包. 注意要安装正确的版本.

7) 以 root 用户身份安装 minigui-res-1.0.02.tar.gz, minigui-fonts-1.0.00.tar.gz, 和 minigui-imetabs-0.9.96.tar.gz 软件包. 这些软件包的安装一般不会出现问题. 此处不再赘述.

8) 在某个目录下解开 libminigui-1.0.00.tar.gz, 并进入新建的 libminigui-1.0.00 目录.

       $ tar zxf libminigui-1.0.00.tar.gz
       $ cd libminigui-1.0.00

9) 依次运行如下命令:

       $ ./autogen.sh ; 如果您的系统中没有安装 autoconf/automake,
                      ; 则不要执行这一步.
       $ ./configure
       $ make

10) 以 root 身份运行 make install 命令:

       $ su -
       # make install

11) 修改 /etc/ld.so.conf 文件, 将 /usr/local/lib 目录添加到该文件最后一行. 修改后类似:

                /usr/lib
                /usr/X11R6/lib
                /usr/i486-linux-libc5/lib
                /usr/local/lib

12) 以 root 身份执行 ldconfig 命令:

       # ldconfig

13) 在新目录中解开 mglite-exec-1.0.02.tar.gz, 并进入新建目录:

       $ tar zxf mglite-exec-1.0.02.tar.gz
       $ cd mglite-exec-1.0.02

14) 依次运行如下命令:

       $ ./autogen.sh ; 如果您的系统中没有安装 autoconf/automake,
                      ; 则不要执行这一步.
       $ ./configure
       $ make

15) 进入 mginit 目录, 并执行 mginit 程序:

       $ cd mginit
       $ ./mginit

16) 如果一切正常, 这时可以看到一个虚拟控制台出现在屏幕上.

17) 切换到 ../demos 目录, 执行其中的程序:

      $ cd ../demos
      $ ./fminigui

18) 如果能够在屏幕上看到一个不断飞动的 GUI 窗口, 则表明一切 OK.

19) 如何关闭这个窗口, 不需要在这里赘述了吧. :)


[目录]


[ 本文件由良友·收藏家自动生成 ]