魏永明
( ymwei@minigui.org
)
自由撰稿人
我们在本系列主题五中曾经详细描述了在 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);