程序5-3 LINEDEMO画一个矩形、一个椭圆、一个圆角矩形和两条线段,不过不是按这一顺序。程序表明了定义封闭区域的函数实际上对这些区域进行了填入,因为在椭圆后面的线被遮住了,结果如图5-12中所示。
程序5-3 LINEDEMO LINEDEMO.C /*--------------------------------------------------------- LINEDEMO.C -- Line-Drawing Demonstration Program (c) Charles Petzold, 1998 ----------------------------------------------------------*/ #include <windows.h> LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("LineDemo") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc= WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground= (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName= NULL ; wndclass.lpszClassName= szAppName ; if (!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("Line Demonstration"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static int cxClient, cyClient ; HDC hdc ; PAINTSTRUCT ps ; switch (message) { case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; Rectangle (hdc, cxClient / 8, cyClient / 8, 7 * cxClient / 8, 7 * cyClient / 8) ; MoveToEx (hdc, 0, 0, NULL) ; LineTo (hdc, cxClient, cyClient) ; MoveToEx (hdc, 0, cyClient, NULL) ; LineTo (hdc, cxClient, 0) ; Ellipse (hdc, cxClient / 8, cyClient / 8, 7 * cxClient / 8, 7 * cyClient / 8) ; RoundRect (hdc, cxClient / 4, cyClient / 4, 3 * cxClient / 4, 3 * cyClient / 4, cxClient / 4, cyClient / 4) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
图5-12 LINEDEMO显示 |
贝塞尔曲线
「曲尺」这个词从前指的是一片木头、橡皮或者金属,用来在纸上画曲线。比如说,如果您有一些不同图点,您想要在它们之间画一条曲线(内插或者外插),您首先将这些点描在绘图纸上,然后,将曲尺定在这些点上,并用铅笔沿着曲尺绕着这些点弯曲的方向画曲线。
当然,时至今日,曲尺已经数学公式化了。有很多种不同的曲尺公式,它们各有千秋。贝塞尔曲线是计算机程序设计中用得最广的曲尺公式之一,它是直到最近才加到操作系统层次的图形支持中的。在六十年代Renault汽车公司进行了由手工设计车体(要用到粘土)到计算机辅助设计的转变。他们需要一些数学工具,而Pierm Bezier找到了一套公式,最后显示出这套公式应付这样的工作非常有用。
此后,二维的贝塞尔曲线成了计算机图学中最有用的曲线(在直线和椭圆之后)。在PostScript中,所有曲线都用贝塞尔曲线表示-椭圆线用贝塞尔曲线来逼近。贝塞尔曲线也用于定义PostScript字体的字符轮廓(TrueType使用一种更简单更快速的曲尺公式)。
一条二维的贝塞尔曲线由四个点定义-两个端点和两个控制点。曲线的端点在两个端点上,控制点就好像「磁石」一样把曲线从两个端点间的直线处拉走。这一点可以由底下的BEZIER互动交谈程序做出最好的展示,如程序5-4所示。
程序5-4 BEZIER BEZIER.C /*----------------------------------------------------------------------- BEZIER.C -- Bezier Splines Demo (c) Charles Petzold, 1998 ------------------------------------------------------------------------*/ #include <windows.h> LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("Bezier") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc= WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground= (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName= NULL ; wndclass.lpszClassName= szAppName ; if (!RegisterClass (&wndclass)) { MessageBox (NULL, TEXT ("Program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("Bezier Splines"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } void DrawBezier (HDC hdc, POINT apt[]) { PolyBezier (hdc, apt, 4) ; MoveToEx (hdc, apt[0].x, apt[0].y, NULL) ; LineTo (hdc, apt[1].x, apt[1].y) ; MoveToEx (hdc, apt[2].x, apt[2].y, NULL) ; LineTo (hdc, apt[3].x, apt[3].y) ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static POINT apt[4] ; HDC hdc ; int cxClient, cyClient ; PAINTSTRUCT ps ; switch (message) { case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; apt[0].x = cxClient / 4 ; apt[0].y = cyClient / 2 ; apt[1].x = cxClient / 2 ; apt[1].y = cyClient / 4 ; apt[2].x = cxClient / 2 ; apt[2].y = 3 * cyClient / 4 ; apt[3].x = 3 * cxClient / 4 ; apt[3].y = cyClient / 2 ; return 0 ; case WM_LBUTTONDOWN: case WM_RBUTTONDOWN: case WM_MOUSEMOVE: if (wParam & MK_LBUTTON || wParam & MK_RBUTTON) { hdc = GetDC (hwnd) ; SelectObject (hdc, GetStockObject (WHITE_PEN)) ; DrawBezier (hdc, apt) ; if (wParam & MK_LBUTTON) { apt[1].x = LOWORD (lParam) ; apt[1].y = HIWORD (lParam) ; } if (wParam & MK_RBUTTON) { apt[2].x = LOWORD (lParam) ; apt[2].y = HIWORD (lParam) ; } SelectObject (hdc, GetStockObject (BLACK_PEN)) ; DrawBezier (hdc, apt) ; ReleaseDC (hwnd, hdc) ; } return 0 ; case WM_PAINT: InvalidateRect (hwnd, NULL, TRUE) ; hdc = BeginPaint (hwnd, &ps) ; DrawBezier (hdc, apt) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
由于这个程序要用到一些在第七章才讲的鼠标处理方式,所以我不在这里讨论它的内部运作(不过,这也是简单的),而是用这个程序来实验性地操纵贝塞尔曲线。在这个程序中,两个顶点设定在显示区域的上下居中、左右位于1/4和3/4处的位置;两个控制点可以改变,按住鼠标左键或右键并拖动鼠标可以分别改动两个控制点之一。图5-13是一个典型的例子。
除了贝塞尔曲线本身,程序还从第一个控制点向左边的第一个端点(也叫做开始点)画一条直线,并从第二个控制点向右边的端点画一条直线。
由于下面几个特点,贝塞尔曲线在计算机辅助设计中非常有用。首先,经过少量练习,就可以把曲线调整到与想要的形状非常接近。
图5-13 BEZIER程序的显示 |
其次,贝塞尔曲线非常好控制。对于有的曲尺种类来说,曲线不经过任何一个定义该曲线的点。贝塞尔曲线总是由其两个端点开始和结束的(这是在推导贝塞尔公式时所做的假设之一)。另外,有些形式的曲尺公式有奇异点,在这些点处曲线趋向无穷远,这在计算机辅助设计中通常是很不合适的。事实上,贝塞尔曲线总是受限于一个四边形(叫做「凸包」),这个四边形由端点和控制点连接而成。
第三个特点涉及端点和控制点之间的关系。曲线总是与第一个控制点到起点的直线相切,并保持同一方向;同时,也与第二个控制点到终点的直线相切,并保持同一方向。这是用于推导贝塞尔公式时所做的另外两个假设。
第四,贝塞尔曲线通常比较具有美感。我知道这是一个主观评价的问题,不过,并非只有我才这样想。
在32位的Windows版本之前,您必须利用Polyline来自己建立贝塞尔曲线,并且还需要知道下面的贝塞尔曲线的参数方程。起点是( x0,y0),终点是( x3,y3),两个控制点是(x1,y1)和(x2,y2),随着t的值从0到1的变化,就可以画出曲线:
x(t) = (1 - t)3 x0 + 3t (1 - t)2 x1 + 3t2 (1 - t) x2 + t3 x3 y(t) = (1 - t)3 y0 + 3t (1 - t)2 y1 + 3t2 (1 - t) y2 + t3 y3
在Windows 98中,您不需要知道这些公式。要画一条或多条连接的贝塞尔曲线,只需呼叫:
PolyBezier (hdc, apt, iCount) ;
或
PolyBezierTo (hdc, apt, iCount) ;
两种情况下,apt都是POINT结构的数组。对PolyBezier,前四个点(按照顺序)给出贝塞尔曲线的起点、第一个控制点、第二个控制点和终点。此后的每一条贝塞尔曲线只需给出三个点,因为后一条贝塞尔曲线的起点就是前一条贝塞尔曲线的终点,如此类推。iCount参数等于1加上您所绘制的这些首尾相接曲线条数的三倍。
PolyBezierTo函数使用目前点作为第一个起点,第一条以及后续的贝塞尔曲线都只需要给出三个点。当函数传回时,目前点设定为最后一个终点。
一点提示:在画一系列相连的贝塞尔曲线时,只有当第一条贝塞尔曲线的第二个控制点、第一条贝塞尔曲线的终点(也就是第二条曲线的起点)和第二条贝塞尔曲线的第一个控制点线性相关时,也就是说这三个点在同一条直线上时,曲线在连接点处才是光滑的。
使用现有画笔(Stock Pens)
当您呼叫这一节中讨论的任何画线函数时,Windows使用设备内容中目前选中的「画笔」来画线。画笔决定线的色彩、宽度和画笔样式,画笔样式可以是实线、点划线或者虚线,内定设备内容中画笔为BLACK_PEN。不管映像方式是什么,这种画笔都画出一个图素宽的黑色实线来。BLACK_PEN是Windows提供的三种现有画笔之一,其它两种是WHITE_PEN和NULL_PEN,NULL_PEN什么都不画。您也可以自己自订画笔。
Windows程序以句柄来使用画笔。 Windows表头文件WINDEF.H中包含一个叫做HPEN的型态定义,即画笔的句柄,可以定义这个型态的变量(例如hPen):
HPEN hPen ;
呼叫GetStockObject,可以获得现有画笔的句柄。例如,假设您想使用名为WHITE_PEN的现有画笔,可以如下取得画笔的句柄:
hPen = GetStockObject (WHITE_PEN) ;
现在必须将画笔选进设备内容:
SelectObject (hdc, hPen) ;
目前的画笔是白色。在这个呼叫后,您画的线将使用WHITE_PEN,直到您将另外一个画笔选进设备内容或者释放设备内容句柄为止。
您也可以不定义hPen变量,而将GetStockObject和SelectObject呼叫合并成一个叙述:
SelectObject (hdc, GetStockObject (WHITE_PEN)) ;
如果想恢复到使用BLACK_PEN的状态,可以用一个叙述取得这种画笔的句柄,并将其选进设备内容:
SelectObject (hdc, GetStockObject (BLACK_PEN)) ;
SelectObject的传回值是此呼叫前设备内容中的画笔句柄。如果启动一个新的设备内容并呼叫
hPen = SelectObject (hdc, GetStockobject (WHITE_PEN)) ;
则设备内容中的目前画笔将为WHITE_PEN,变量hPen将会是BLACK_PEN的句柄。以后通过呼叫
SelectObject (hdc, hPen) ;
就能够将BLACK_PEN选进设备内容。
画笔的建立、选择和删除
尽管使用现有画笔非常方便,但却受限于实心的黑画笔、实心的白画笔或者没有画笔这三种情况。如果想得到更丰富多彩的效果,就必须建立自己的画笔。
这一过程通常是:使用函数CreatePen或CreatePenIndirect建立一个「逻辑画笔」,这仅仅是对画笔的描述。这些函数传回逻辑画笔的句柄;然后,呼叫SelectObject将画笔选进设备内容。现在,就可以使用新的画笔来画线了。在任何时候,都只能有一种画笔选进设备内容。在释放设备内容(或者在选择了另一种画笔到设备内容中)之后,就可以呼叫DeleteObject来删除所建立的逻辑画笔了。在删除后,该画笔的句柄就不再有效了。
逻辑画笔是一种「GDI对象」,它是您可以建立的六种GDI对象之一,其它五种是画刷、位图、区域、字体和调色盘。除了调色盘之外,这些对象都是通过SelectObject选进设备内容的。
在使用画笔等GDI对象时,应该遵守以下三条规则:
- 最后要删除自己建立的所有GDI对象。
- 当GDI对象正在一个有效的设备内容中使用时,不要删除它。
- 不要删除现有对象。
这些规则当然是有道理的,而且有时这道理还挺微妙的。下面我们将举些例子来帮助理解这些规则。
CreatePen函数的语法形如:
hPen = CreatePen (iPenStyle, iWidth, crColor) ;
其中,iPenStyle参数确定画笔是实线、点线还是虚线,该参数可以是WINGDI.H表头文件中定义的以下标识符,图5-14显示了每种画笔产生的画笔样式。
图5-14 七种画笔样式 |
对于PS_SOLID、PS_NULL和PS_INSIDEFRAME画笔样式,iWidth参数是画笔的宽度。iWidth值为0则意味着画笔宽度为一个图素。现有画笔是一个图素宽。如果指定的是点划线或者虚线式画笔样式,同时又指定一个大于1的实际宽度,那么Windows将使用实线画笔来代替。
CreatePen的crColor参数是一个COLORREF值,它指定画笔的颜色。对于除了PS_INSIDEFRAME之外的画笔样式,如果将画笔选入设备内容中,Windows会将颜色转换为设备所能表示的最相近的纯色。PS_INSIDEFRAME是唯一一种可以使用混色的画笔样式,并且只有在宽度大于1的情况下才如此。
在与定义一个填入区域的函数一起使用时,PS_INSIDEFRAME画笔样式还有另外一个奇特之处:对于除了PS_INSIDEFRAME以外的所有画笔样式来说,如果用来画边界框的画笔宽度大于1个图素,那么画笔将居中对齐在边界框在线,这样边界框线的一部分将位于边界框之外;而对于PS_INSIDEFRAME画笔样式来说,整条边界框线都画在边界框之内。
您也可以通过建立一个型态为LOGPEN(「逻辑画笔」)的结构,并呼叫CreatePenIndirect来建立画笔。如果您的程序使用许多能在原始码中初始化的画笔,那么使用这种方法将有效得多。
要使用CreatePenIndirect,首先定义一个LOGPEN型态的结构:
LOGPEN logpen ;
此结构有三个成员:lopnStyle(无正负号整数或UINT)是画笔样式,lopnWidth(POINT结构)是按逻辑单位度量的画笔宽度,lopnColor (COLORREF)是画笔颜色。Windows只使用lopnWidth结构的x值作为画笔宽度,而忽略y值。
将结构的地址传递给CreatePenIndirect结构就可以建立画笔了:
hPen = CreatePenIndirect (&logpen) ;
注意,CreatePen和CreatePenIndirect函数不需要设备内容句柄作为参数。这些函数建立与设备内容没有联系的逻辑画笔。直到呼叫SelectObject之后,画笔才与设备内容发生联系。因此,可以对不同的设备(如屏幕和打印机)使用相同的逻辑画笔。
下面是建立、选择和删除画笔的一种方法。假设您的程序使用三种画笔-一种宽度为1的黑画笔、一种宽度为3的红画笔和一种黑色点式画笔,您可以先定义三个变量来存放这些画笔的句柄:
static HPEN hPen1, hPen2, hPen3 ;
在处理WM_CREATE期间,您可以建立这三种画笔:
hPen1 = CreatePen (PS_SOLID, 1, 0) ; hPen2 = CreatePen (PS_SOLID, 3, RGB (255, 0, 0)) ; hPen3 = CreatePen (PS_DOT, 0, 0) ;
在处理WM_PAINT期间,或者是在拥有一个设备内容有效句柄的任何时间里,您都可以将这三个画笔之一选进设备内容并用它来画线:
SelectObject (hdc, hPen2) ;
画线函数
SelectObject (hdc, hPen1) ;
其它画线函数
在处理WM_DESTROY期间,您可以删除您建立的三种画笔:
DeleteObject (hPen1) ; DeleteObject (hPen2) ; DeleteObject (hPen3) ;
这是建立、选择和删除画笔最直接的方法。但是您的程序必须知道执行期间需要哪些逻辑画笔,为此,您可能想要在每个WM_PAINT消息处理期间建立画笔,并在呼叫EndPaint之后删除它们(您可以在呼叫EndPaint之前删除它们,但是要小心,不要删除设备内容中目前选择的画笔)。
您可能还希望随时建立画笔,并将CreatePen和SelectObject呼叫组合到同一个叙述中:
SelectObject (hdc, CreatePen (PS_DASH, 0, RGB (255, 0, 0))) ;
现在再开始画线,您将使用一个红色虚线画笔。在画完红色虚线之后,可以删除画笔。糟了!由于没有保存画笔句柄,怎么才能删除这些画笔呢?不要紧,请记住,SelectObject将传回设备内容中上一次选择的画笔句柄。所以,您可以通过呼叫SelectObject将BLACK_PEN选进设备内容,并删除从SelectObject传回的值:
DeleteObject (SelectObject (hdc, GetStockObject (BLACK_PEN))) ;
下面是另一种方法,在将新建立的画笔选进设备内容时,保存SelectObject传回的画笔句柄:
hPen = SelectObject (hdc, CreatePen (PS_DASH, 0, RGB (255, 0, 0))) ;
现在hPen是什么呢?如果这是在取得设备内容之后第一次呼叫SelectObject,则hPen是BLACK_PEN对象的句柄。现在,可以将hPen选进设备内容,并删除所建立的画笔(第二次SelectObject呼叫传回的句柄),只要一道叙述即可:
DeleteObject (SelectObject (hdc, hPen)) ;
如果有一个画笔的句柄,就可以通过呼叫GetObject取得LOGPEN结构各个成员的值:
GetObject (hPen, sizeof (LOGPEN), (LPVOID) &logpen) ;
如果需要目前选进设备内容的画笔句柄,可以呼叫:
hPen = GetCurrentObject (hdc, OBJ_PEN) ;
在第十七章将讨论另一个建立画笔的函数ExtCreatePen。
填入空隙
使用点式画笔和虚线画笔会产生一个有趣的问题:点和虚线之间的空隙会怎样呢?您所需要的是什么?
空隙的着色取决于设备内容的两个属性-背景模式和背景颜色。内定背景模式为OPAQUE,在这种方式下,Windows使用背景色来填入空隙,内定的背景色为白色。这与许多程序在窗口类别中用WHITE_BRUSH来擦除窗口背景的做法是一致的。
您可以通过如下呼叫来改变Windows用来填入空隙的背景色:
SetBkColor (hdc, crColor) ;
与画笔色彩所使用的crColor参数一样,Windows将这里的背景色转换为纯色。可以通过用GetBkColor来取得设备内容中定义的目前背景色。
通过将背景模式转换为TRANSPARENT,可以阻止Windows填入空隙:
SetBkMode (hdc, TRANSPARENT) ;
此后,Windows将忽略背景色,并且不填入空隙,可以通过呼叫GetBkMode来取得目前背景模式(TRANSPARENT或者OPAQUE)。
绘图方式
设备内容中定义的绘图方式也影响显示器上所画线的外观。设想画这样一条直线,它的色彩由画笔色彩和画线区域原来的色彩共同决定。设想用同一种画笔在白色表面上画出黑线而在黑色表面上画出白线,而且不用知道表面是什么色彩。这样的功能对您有用吗?通过绘图方式的设定,这些都可以实作。
当Windows使用画笔来画线时,它实际上执行画笔图素与目标位置处原来图素之间的某种位布尔运算。图素间的位布尔运算叫做「位映像运算」,简称为「ROP」。由于画一条直线只涉及两种图素(画笔和目标),因此这种布尔运算又称为「二元位映像运算」,简记为「ROP2」。Windows定义了16种ROP2代码,表示Windows组合画笔图素和目标图素的方式。在内定设备内容中,绘图方式定义为R2_COPYPEN,这意味着Windows只是将画笔图素复制到目标图素,这也是我们通常所熟知的。此外,还有15种ROP2码。
16种不同的ROP2码是怎样得来的呢?为了示范的需要,我们假设使用单色系统,目标色(窗口显示区域的色彩)为黑色(用0来表示)或者白色(用1来表示),画笔也可以为黑色或者白色。用黑色或者白色画笔在黑色或者白色目标上画图有四种组合:白笔与白目标、白笔与黑目标、黑笔与白目标、黑笔与黑目标。
画笔在目标上绘制后会得到什么呢?一种可能是不管画笔和目标的色彩,画出的线总是黑色的,这种绘图方式由ROP2代码R2_BLACK表示。另一种可能是只有当画笔与目标都为黑色时,画出的结果才是白色,其它情况下画出的都是黑色。尽管这似乎有些奇怪,Windows还是为这种方式起了一个名字,叫做R2_NOTMERGEPEN。Windows执行目标图素与画笔图素的位「或」运算,然后翻转所得色彩。
表5-2显示了所有16种ROP2绘图方式,表中指示了画笔色彩(P)与目标色彩(D)是如何组合而成结果色彩的。在标有「布尔操作」的那一栏中,用C语言的表示法给出了目标图素与画笔图素的组合方式。
表5-2 |
画笔(P):目标(D): |
1 1 |
1 0 |
0 1 |
0 0 |
布尔操作 |
绘图模式 |
结果: |
0 |
0 |
0 |
0 |
0 |
R2_BLACK |
0 |
0 |
0 |
1 |
~(P | D) |
R2_NOTMERGEPEN | |
0 |
0 |
1 |
0 |
~P & D |
R2_MASKNOTPEN | |
0 |
0 |
1 |
1 |
~P |
R2_NOTCOPYPEN | |
0 |
1 |
0 |
0 |
P & ~D |
R2_MASKPENNOT | |
0 |
1 |
0 |
1 |
~D |
R2_NOT | |
0 |
1 |
1 |
0 |
P ^ D |
R2_XORPEN | |
0 |
1 |
1 |
1 |
~(P & D) |
R2_NOTMASKPEN | |
1 |
0 |
0 |
0 |
P & D |
R2_MASKPEN | |
1 |
0 |
0 |
1 |
~(P ^ D) |
R2_NOTXORPEN | |
1 |
0 |
1 |
0 |
D |
R2_NOP | |
1 |
0 |
1 |
1 |
~P | D |
R2_MERGENOTPEN | |
1 |
1 |
0 |
0 |
P |
R2_COPYPEN(内定) | |
1 |
1 |
0 |
1 |
P | ~D |
R2_MERGEPENNOT | |
1 |
1 |
1 |
0 |
P | D |
R2_MERGEPEN | |
1 |
1 |
1 |
1 |
1 |
R2_WHITE |
可以通过以下呼叫在设备内容中设定新的绘图模式:
SetROP2 (hdc, iDrawMode) ;
iDrawMode参数是表中「绘图模式」一栏中给出的值之一。您可以用函数:
iDrawMode = GetROP2 (hdc) ;
来取得目前绘图方式。设备内容中的内定设定为R2_COPYPEN,它用画笔色彩替代目标色彩。在R2_NOTCOPYPEN方式下,若画笔为黑色,则画成白色;若画笔为白色,则画成黑色。R2_BLACK方式下,不管画笔和背景色为何种色彩,总是画成黑色。与此相反,R2_WHITE方式下总是画成白色。R2_NOP方式就是「不操作」,让目标保持不变。
现在,我们已经讨论了单色系统。然而,大多数系统是彩色的。在彩色系统中,Windows为画笔和目标图素的每个颜色位执行绘图方式的位运算,并再次使用上表描述的16种ROP2代码。R2_NOT绘图方式总是翻转目标色彩来决定线的颜色,而不管画笔的色彩是什么。例如,在青色目标上的线会变成紫色。R2_NOT方式总是产生可见的画笔,除非画笔在中等灰度的背景上绘图。我将在 第七章的BLOKOUT程序中展示R2_NOT绘图方式的使用。
现在再更进一步,从画线到画图形。Windows中七个用来画带边缘的填入图形的函数列于表5-3中。
表5-3 |
函数 |
图形 |
Rectangle |
直角矩形 |
Ellipse |
椭圆 |
RoundRect |
圆角矩形 |
Chord |
椭圆周上的弧,两端以弦连接 |
Pie |
椭圆上的饼图 |
Polygon |
多边形 |
PolyPolygon |
多个多边形 |
Windows用设备内容中选择的目前画笔来画图形的边界框,边界框还使用目前背景方式、背景色彩和绘图方式,这跟Windows画线时一样。关于直线的一切也适用于这些图形的边界框。
图形以目前设备内容中选择的画刷来填入。内定情况下,使用现有对象,这意味着图形内部将画为白色。Windows定义六种现有画刷:WHITE_BRUSH、LTGRAY_BRUSH、GRAY_BRUSH、DKGRAY_BRUSH、BLACK_BRUSH和NULL_BRUSH (也叫HOLLOW_BRUSH)。您可以将任何一种现有画刷选入您的设备内容中,就和您选择一种画笔一样。Windbws将HBRUSH定义为画刷的句柄,所以可以先定义一个画刷句柄变量:
HBRUSH hBrush ;
您可以通过呼叫GetStockObject来取得GRAY_BRUSH的句柄:
hBrush = GetStockObject (GRAY_BRUSH) ;
您可以呼叫SelectObject将它选进设备内容:
SelectObject (hdc, hBrush) ;
现在,如果您要画上表中的任一个图形,则其内部将为灰色。
如果您想画一个没有边界框的图形,可以将NULL_PEN选进设备内容:
SelectObject (hdc, GetStockObject (NULL_PEN)) ;
如果您想画出图形的边界框,但不填入内部,则将NULL_BRUSH选进设备内容:
SelectObject (hdc, GetStockobject (NULL_BRUSH) ;
您也可以自订画刷,就如同您自订画笔一样。我们将马上谈到这个问题。
Polygon函数和多边形填入方式
我已经讨论过了前五个区域填入函数,Polygon是第六个画带边界框的填入图形的函数,该函数的呼叫与Polyline函数相似:
Polygon (hdc, apt, iCount) ;
其中,apt参数是POINT结构的一个数组,iCount是点的数目。如果该数组中的最后一个点与第一个点不同,则Windows将会再加一条线,将最后一个点与第一个点连起来(在Polyline函数中,Windows不会这么做)。PolyPolygon函数如下所示:
PolyPolygon (hdc, apt, aiCounts, iPolyCount) ;
该函数绘制多个多边形。最后一个参数给出了所画的多边形的个数。对于每个多边形,aiCounts数组给出了多边形的端点数。apt数组具有全部多边形的所有点。除传回值以外,PolyPolygon在功能上与下面的代码相同:
for (i = 0, iAccum = 0 ; i < iPolyCount ; i++) { Polygon (hdc, apt + iAccum, aiCounts[i]) ; iAccum += aiCounts[i] ; }
对于Polygon和PolyPolygon函数,Windows使用定义在设备内容中的目前画刷来填入这个带边界的区域。至于填入内部的方式,则取决于多边形填入方式,您可以用SetPolyFillMode函数来设定:
SetPolyFillMode (hdc, iMode) ;
内定情况下,多边形填入方式是ALTERNATE,但是您可以将它设定为WINDING。两种方式的区别参见图5-15所示。
图5-15 用两种多边形填入方式画出的图:ALTERNATE(左)和WINDING(右) |
首先,ALTERNATE和WINDING方式之间的区别很容易察觉。对于ALTERNATE方式,您可以设想从一个无穷大的封闭区域内部的点画线,只有假想的线穿过了奇数条边界线时,才填入封闭区域。这就是填入了星的角而中心没被填入的原因。
五角星的例子使得WINDING方式看起来比实际上更简单一些。在绘制单个的多边形时,大多数情况下,WINDING方式会填入所有封闭的区域。但是也有例外。
在WINDING方式下要确定一个封闭区域是否被填入,您仍旧可以设想从那个无穷大的区域画线。如果假想的线穿过了奇数条边界线,区域就被填入,这和ALTERNATE方式一样。如果假想的线穿过了偶数条边界线,则区域可能被填入也可能不被填入。如果一个方向(相对于假想线)的边界线数与另一个方向的边界线数不相等,就填入区域。
例如,考虑图5-16中的物体。在线的箭头指出了画线的方向。两种方式都会填入三个封闭的L形区域,号码从1到3。号码为4和5的两个小内部区域,在ALTERNATE方式下不会被填入。但是,在WINDING方式下,号码为5的区域会被填入,因为从区域内必须穿过两条相同方向的线才能到达图形外部。号码为4的区域不会被填入,因为必须穿过两条方向相反的线。
如果您怀疑Windows没有这么聪明,那么程序5-5 ALTWIND会展示给您看。
图5-16 WINDING方式不能填入所有内部区域的图形 |
程序5-5 ALTWIND ALTWIND.C /*------------------------------------------------------------------- ALTWIND.C -- Alternate and Winding Fill Modes (c) Charles Petzold, 1998 -------------------------------------------------------------------*/ #include <windows.h> LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("AltWind") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc= WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground= (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName= NULL ; wndclass.lpszClassName= szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("Program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("Alternate and Winding Fill Modes"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static POINT aptFigure [10] = {10,70, 50,70, 50,10, 90,10, 90,50, 30,50, 30,90, 70,90, 70,30, 10,30 }; static int cxClient, cyClient ; HDC hdc ; int i ; PAINTSTRUCT ps ; POINT apt[10] ; switch (message) { case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; SelectObject (hdc, GetStockObject (GRAY_BRUSH)) ; for (i = 0 ; i < 10 ; i++) { apt[i].x = cxClient * aptFigure[i].x / 200 ; apt[i].y = cyClient * aptFigure[i].y / 100 ; } SetPolyFillMode (hdc, ALTERNATE) ; Polygon (hdc, apt, 10) ; for (i = 0 ; i < 10 ; i++) { apt[i].x += cxClient / 2 ; } SetPolyFillMode (hdc, WINDING) ; Polygon (hdc, apt, 10) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
图形的坐标(划分为100×100个单位)储存在aptFigure数组中。这些坐标是依据显示区域的宽度和高度划分的。程序显示图形两次,一次使用ALTERNATE填入方式,另一次使用WINDING方式。结果见图5-17。
图5-17 ALTWIND的显示 |
用画刷填入内部
Rectangle、RoundRect、Ellipse、Chord、Pie、Polygon和PolyPolygon图形的内部是用选进设备内容的目前画刷(也称为「图样」)来填入的。画刷是一个8×8的位图,它水平和垂直地重复使用来填入内部区域。
当Windows用混色的方法来显示多于可从显示器上得到的色彩时,实际上是将画刷用于色彩。在单色系统上,Windows能够使用黑色和白色图素的混色建立64种不同的灰色,更精确地说,Windows能够建立64种不同的单色画刷。对于纯黑色,8×8位图中的所有位均为0。第一种灰色有一位为1,第二种灰色有两位为1,以此类推,直到8×8位图中所有位均为1,这就是白色。在16色或256色显示系统上,混色也是位图,并且可以得到更多的色彩。
Windows还有五个函数,可以让您建立逻辑画刷,然后就可使用SelectObject将画刷选进设备内容。与逻辑画笔一样,逻辑画刷也是GDI对象。您建立的所有画刷都必须被删除,但是当它还在设备内容中时不能将其删除。
下面是建立逻辑画刷的第一个函数:
hBrush = CreateSolidBrush (crColor) ;
函数中的Solid并不是指画刷为纯色。在将画刷选入设备内容中时,Windows建立一个混色色的位图,并为画刷使用该位图。
您还可以使用由水平、垂直或者倾斜的线组成的「影线标记(hatch marks)」来建立画刷,这种风格的画刷对着色条形图的内部和在绘图机上进行绘图最有用。建立影线画刷的函数为:
hBrush = CreateHatchBrush (iHatchStyle, crColor) ;
iHatchStyle参数描述影线标记的外观。图5-18显示了六种可用的影线标记风格。
图5-18 六种影线画刷风格 |
CreateHatchBrush中的crColor参数是影线的色彩。在将画刷选进设备内容时,Windows将这种色彩转换为与之最相近的纯色。影线之间的区域根据设备内容中定义的背景方式和背景色来着色。如果背景方式为OPAQUE,则用背景色(它也被转换为纯色)来填入线之间的空间。在这种情况下,影线和填入色都不能是混色而成的颜色。如果背景方式为TRANSPARENT,则Windows只画出影线,不填入它们之间的区域。
您也可以使用CreatePatternBrush和CreateDIBPatternBrushPt建立自己的位图画刷。
建立逻辑画刷的第五个函数包含其它四个函数:
hBrush = CreateBrushIndirect (&logbrush) ;
变量logbrush是一个型态为LOGBRUSH(「逻辑画刷」)的结构,该结构的三个字段如表5-4所示,lbStyle字段的值确定了Windows如何解释其它两个字段的值:
表5-4 |
lbStyle (UINT) |
lbColor (COLORREF) |
lbHatch (LONG) |
BS_SOLID |
画刷的色彩 |
忽略 |
BS_HOLLOW |
忽略 |
忽略 |
BS_HATCHED |
影线的色彩 |
影线画刷风格 |
BS_PATTERN |
忽略 |
位图的句柄 |
BS_DIBPATTERNPT |
忽略 |
指向DIB的指标 |
前面我们用SelectObject将逻辑画笔选进设备内容,用DeleteObject删除画笔,用GetObject来取得逻辑画笔的信息。对于画刷,同样能使用这三个函数。一旦您取得到了画刷句柄,就可以使用SelectObject将该画刷选进设备内容:
SelectObject (hdc, hBrush) ;
然后,您可以使用DeleteObject函数删除所建立的画刷:
DeleteObject (hBrush) ;
但是,不要删除目前选进设备内容的画刷。
如果您需要取得画刷的信息,可以呼叫GetObject:
GetObject (hBrush, sizeof (LOGBRUSH), (LPVOID) &logbrush) ;
其中,logbrush是一个型态为LOGBRUSH的结构。
到目前为止,所有的程序都是相对于显示区域的左上角,以图素为单位绘图的。这是内定情况,但不是唯一选择。事实上,「映像方式」是一种几乎影响任何显示区域绘图的设备内容属性。另外有四种设备内容属性-窗口原点、视端口原点、窗口范围和视端口范围-与映像方式密切相关。
大多数GDI绘图函数需要坐标值或大小。例如,下面是TextOut函数:
TextOut (hdc, x, y, psText, iLength) ;
参数x和y分别表示文字的开始位置。参数x是在水平轴上的位置,参数y是在垂直轴上的位置,通常用(x,y)来表示这个点。
在TextOut中,以及在几乎所有GDI函数中,这些坐标值使用的都是一种「逻辑单位」。Windows必须将逻辑单位转换为「设备单位」,即图素。这种转换是由映像方式、窗口和视端口的原点以及窗口和视端口的范围所控制的。映像方式还指示着x轴和y轴的方向(orientation);也就是说,它确定了当您在向显示器的左或者右移动时x的值是增大还是减小,以及在上下移动时y的值是增大还是减小。
Windows定义了8种映像方式,它们在WINGDI.H中相应的标识符和含义如表5-5所示。
表5-5 |
映像方式 |
逻辑单位 |
增加值 | |
x值 |
y值 | ||
MM_TEXT |
图素 |
右 |
下 |
MM_LOMETRIC |
0.1 mm |
右 |
上 |
MM_HIMETRIC |
0.01 mm |
右 |
上 |
MM_LOENGLISH |
0.01 in. |
右 |
上 |
MM_HIENGLISH |
0.001 in. |
右 |
上 |
MM_TWIPS |
1/1440 in. |
右 |
上 |
MM_ISOTROPIC |
任意(x = y) |
可选 |
可选 |
MM_ANISOTROPIC |
任意(x != y) |
可选 |
可选 |
METRIC和ENGLISH指一般通行的度量衡系统,点是印刷的测量单位,约等于1/72英寸,但在图形程序设计中假定为正好1/72英寸。「Twip」等于1/20点,也就是1/1440英寸。「Isotropic」和「anisotropic」是真正的单字,意思是「等方性」(同方向)和「异方性」(不同方向)。
您可以使用下面的叙述来设定映射方式:
SetMapMode (hdc, iMapMode) ;
其中,iMapMode是8个映像方式标识符之一。您可以通过以下呼叫取得目前的映像方式:
iMapMode = GetMapMode (hdc) ;
内定映像方式为MM_TEXT。在这种映像方式下,逻辑单位与实际单位相同,这样我们可以直接以图素为单位进行操作。在TextOut呼叫中,它看起来像这样:
TextOut (hdc, 8, 16, TEXT ("Hello"), 5) ;
文字从距离显示区域左端8图素、上端16图素的位置处开始。
如果映像方式设定为MM_LOENGLISH:
SetMapMode (hdc, MM_LOENGLISH) ;
则逻辑单位是百分之一。现在,TextOut呼叫如下:
TextOut (hdc, 50, -100, TEXT ("Hello"), 5) ;
文字从距离显示区域左端0.5英寸、上端1英寸的位置处开始。至于y坐标前面的负号,随着我们对映像方式更详细的讨论,将逐渐清楚。其它映像方式允许程序按照毫米、打印机的点大小或者任意单位的坐标轴来指定坐标。
如果您认为使用图素进行工作很合适,那么就不要使用内定的MM_TEXT方式外的任何映像方式。如果需要以英寸或者毫米尺寸显示图像,那么可以从GetDeviceCaps中取得所需要的信息,自己再进行缩放。其它映像方式都是避免您自己进行缩放的一个方便途径而已。
虽然您在GDI函数中指定的坐标是32位的值,但是仅有Windows NT能够处理全32位。在Windows 98中,坐标被限制为16位,范围从-32,768到32,767。一些使用坐标表示矩形的开始点和结束点的Windows函数也要求矩形的宽和高小于或者等于32,767。