Windows编程 第七回 绘图课(上)
-----路过的朋友,若发现错误或有好的建议,欢迎在下面留言,谢谢!-----
之前讲了很多GDI和设备描述表的内容,想必大家对这么多枯燥晦涩的东西早就感到厌倦了吧。为了激发一下大家继续学习Windows的兴趣,这回就给大家展示一些“有趣”的东西吧。
画点(写像素)
先前总是说GDI很重要,功能很强大,快把它捧得天花乱坠了,让人听着总感觉有点“悬”。现在我们就从绘制图形中最简单的画点来开始具体地使用一下它吧。
说到画点就不得不提一下SetPixel和GetPixel两个函数。(读者可以试着从名字来猜一下它们的功能,API的很多函数都可以从名字大概地推测出它们的用途)
SetPixel函数在指定的x和y坐标以特定的颜色设置像素:
SetPixel (hdc, x, y, crColor);
如同在任何绘图函数中一样,第一个参数是设备描述表的句柄。第二个和第三个参数指明了坐标位置。通常要获得窗口客户区的设备描述表,并且x和y相对于该客户区的左上角。最后一个参数是COLORREF类型①,用于指定颜色。如果在函数中指定的颜色在显示器不能被不支持,则函数将像素设置为最接近的纯色并从函数返回该值。
GetPixel函数返回指定坐标处的像素颜色:
COLORREF crColor = GetPixel (hdc, x, y) ;
理论上,有了这两个函数我们就可以绘制一切图形了。任何图形不都是由点组成的吗?但实际情况中如果要绘制一条直线或一个矩形等“复杂”一点的图形,我们会用专门的函数。(当然你也可以使用SetPixel函数来完成,只要你不嫌麻烦,并不断地调整x和y坐标)显然专门的函数要比数次调用SetPixel函数要简便得多,而且性能更佳。因此,Windows GDI虽然包含了SetPixel和GetPixel两个函数,但我们平时绘图很少使用他们。
那我们接下来看一下常用的“专门的函数”吧。
与画直线相关的
画一条直线,必须调用两个函数。第一个函数指定了线的开始点,第二个函数指定了线的终点:
MoveToEx (hdc, xBeg, yBeg, NULL) ;
LineTo (hdc, xEnd, yEnd) ;
MoveToEx实际上不会画线,它只是设定了设备描述表的“当前位置”属性。MoveToEx的最后一个参数是指向POINT结构②的指针。从该函数返回后,POINT结构的x和y字段指出了先前的当前位置。如果你不需要这种信息(通常如此),可以简单地像上面的例子那样将最后一个参数设定为NULL。
LineTo函数才负责从当前的位置到它所指定的点画一条直线。在默认的设备描述表中,当前位置最初设定在点(0,0)。如果在调用LineTo之前没有设置当前位置,那么它将从客户区的左上角开始画线。③
我们来个例子吧:尝试画一个矩形。
我们可以先定义一个数组
POINTapt[5] = { 100, 100, 200, 100, 200, 200, 100, 200, 100, 100 } ;
注意,最后一个点与第一个点相同。现在,只需要使用MoveToEx函数把当前位置移到第一个点,并对后面的点使用LineTo:
MoveToEx(hdc, apt[0].x, apt[0].y, NULL) ;
for (i = 1 ; i < 5 ; i++)
LineTo (hdc, apt[i].x, apt[i].y) ;
你是否觉得有些奇怪呢,只调用一次MoveToEx函数把当前位置设置在(100,100),后面四次调用LineTo函数画线,似乎是画了三条以(100,100)为起点的射线呀(第四次画线的起点与终点相同)。代码是对的,确实是画了一个矩形。只是关于LineTo函数有一点我们没有讲:LineTo函数每次画完线后会不断更新当前位置为线的终点。
当你要将数组中的点连接成线时,使用Polyline函数会简单得多。下面这条语句画出与上面一段代码相同的矩形:
Polyline (hdc, apt, 5) ;
最后一个参数是点的数目。我们还可以使用(sizeof (apt) / sizeof (POINT))来表示这个值。Polyline与一个MoveToEx函数后面加几个LineTo函数的效果相同,但是,Polyline既不使用也不改变当前位置。而PolylineTo有些不同,这个函数使用当前位置作为开始点,并将当前位置设定为最后一根线的终点。下面的程序代码画出与上面所示一样的矩形:
MoveToEx (hdc, apt[0].x, apt[0].y, NULL) ;
PolylineTo (hdc, apt + 1, 4) ;//apt+1指向apt[1]结构项
你可以对几条线使用Polyline和PolylineTo,这些函数在绘制复杂曲线最有用了。你使用由几百甚至几千条极短线段,把它们恰当的连在一起来绘制曲线,例如正弦曲线等。
与画矩形相关的
我们先来认识一下绘制矩形的函数:
Rectangle (hdc, xLeft, yTop,xRight, yBottom) ;
点(xLeft,yTop)是矩形的左上角,(xRight, yBottom)是矩形的右下角。用函数Rectangle画出的图形如图所示,矩形的边总是平行于显示器的水平和垂直边。
你知道了如何画矩形,也就知道了如何画椭圆,因为它们使用的参数都是相同的:
Ellipse (hdc, xLeft, yTop,xRight, yBottom) ;
看图之后也许你就会明白了吧,在Windows里椭圆是通过给定与它相切的矩形框来间接确定的。
大家再来瞧一瞧画圆角的矩形的函数吧:
RoundRect (hdc, xLeft, yTop,xRight, yBottom,xCornerEllipse, yCornerEllipse) ;
没错,你又会发现此函数使用与函数Rectangle及Ellipse相同的边界框。Windows使用一个小椭圆来画圆角,这个椭圆的宽为xCornerEllipse,高为yCornerEllipse。xCornerEllipse和yCornerEllipse的值越大,角就越明显。如果xCornerEllipse等于xRight与xLeft的差,且yCornerEllipse等于yTop与yBottom的差,那么RoundRect函数将画出一个椭圆。这是一种简单的方法,但是结果看起来有点不对劲,因为角的弯曲部分在矩形长的一边要大些。要矫正这一问题,你可以让xCornerEllipse与yCornerEllipse的值相等。
最后,看到下面一句话你可能会吃惊:其实还有三个函数与上面三个函数使用相同的边界框。
Arc(hdc, xLeft, yTop, xRight,yBottom, xStart, yStart, xEnd, yEnd) ;
Chord(hdc, xLeft, yTop,xRight, yBottom, xStart, yStart, xEnd, yEnd) ;
Pie(hdc, xLeft, yTop, xRight,yBottom, xStart, yStart, xEnd, yEnd) ;
Arc函数用来画椭圆线。Windows用一条假想的线将(xStart, yStart)与椭圆的中心连接,从该线与边界框的交点开始,Windows按逆时针方向,沿着椭圆画一条弧。Windows还用另一条假想的线将(xEnd,yEnd)与椭圆的中心连接,在该线与边界框的交点处,Windows停止画弧。
Chord函数用相同的方法用来画椭圆弓形。
而Pie函数则用相同的方法用来画椭圆扇形。
你可能不太明白在Arc、Chord和Pie函数中开始和结束位置的用法,为什么不简单地在椭圆的周线线指定开始和结束点呢?是的,你可以这么做,但是你将不得不算出这些点。Windows的方法在不要求这种精确性的条件下,却完成了相同的工作。
可能会有些可爱的小盆友问如果要画正方形、圆、圆角正方形……肿么办?
把上面的参数改一下不就可以了么。
画“彩图”
很不幸,如果你只用上面的函数绘图,你会发现最终显示在屏幕上的图形都是一样粗细的黑色线条,这是系统默认的。你可能会觉得很单调,还好我们有办法解决这个问题。微软为我们提供了“画笔”。它可以决定绘图线条的色彩、宽度和线型。
使用现有画笔(Windows预定义的一些常用的画笔)
当你调用本回任一个画线函数时,Windows使用设备描述表中当前选中的“画笔”来画线。默认设备描述表中的画笔为BLACK_PEN。这种画笔只画出一个像素宽的黑色实线。另外两种Windows提供的现有画笔是WHITE_PEN和NULL_PEN。其功能从名字就可以看出,前者就是画出一个像素宽的白色实线,后者什么都不画(那它有什么作用?下一回你就知道了)。
Windows程序使用HPEN类型的句柄来引用画笔。可以这样定义这个类型的变量:
HPENhPen;
调用GetStockObject,可以获得现有画笔的句柄。例如,假设你想使用名为WHITE_PEN的现有画笔,可以如下取得画笔的句柄:
hPen = (HPEN)GetStockObject (WHITE_PEN) ; //这里需要强制类型转换
接着你就要将画笔选进设备描述表:
SelectObject (hdc, hPen) ;
此时当前的画笔才是白色。在这个调用后,你画的线将使用WHITE_PEN,直到你将另外一个画笔选进设备描述表或者释放设备描述表句柄为止。
你也可以不定义hPen变量,而将GetStockObject和SelectObject调用合并成一个语句:
SelectObject (hdc, (HPEN)GetStockObject (WHITE_PEN)) ;
如果想恢复到使用BLACK_PEN的状态,可以用一个语句取得这种画笔的句柄,并将其选进设备描述表:
SelectObject (hdc, (HPEN)GetStockObject (BLACK_PEN)) ;
SelectObject的返回值是此调用前设备描述表中的画笔句柄。如果启动一个新的设备描述表并调用
hPen = (HPEN)SelectObject (hdc,(HPEN)GetStockobject (WHITE_PEN)) ;
则设备描述表中的当前画笔将为WHITE_PEN,变量hPen将会是BLACK_PEN的句柄。以后通过调用
SelectObject (hdc, hPen) ;
就能够将BLACK_PEN选进设备描述表。
画笔的创建、选择和删除
尽管使用现有画笔非常方便,但却受限于实心的黑画笔、实心的白画笔或者没有画笔这三种情况。如果想得到更丰富多彩的效果,就必须创建自己的画笔。
这一过程通常是:
首先使用函数CreatePen或CreatePenIndirect创建一个“逻辑画笔”,这仅仅是对画笔的描述。这些函数返回逻辑画笔的句柄。
然后,调用SelectObject将画笔选进设备描述表。现在,就可以使用新的画笔来画线了。在任何时候,都只能有一种画笔选进设备描述表。
最后,在释放设备描述表(或者在选择了另一种画笔到设备内容中)之后,就可以调用DeleteObject来删除所建立的逻辑画笔了。在删除后,该画笔的句柄就不再有效了。
这个过程确实有点繁琐,不过我们没办法改变,还给听微软的。
CreatePen函数的语法形如:
hPen = CreatePen (iPenStyle,iWidth, crColor) ;
其中,iPenStyle参数确定画笔是实线、点线还是虚线④,iWidth参数是画笔宽度,crColor参数是一个COLORREF值,它指定画笔的颜色。注:如果指定画笔是点划线或虚线,则线宽必须不能大于1,否则Windows将使用实线画笔代替。
你也可以通过建立一个类型为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) ;
[line-drawing functions]
SelectObject (hdc, hPen1) ;
[line-drawing functions]
在处理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)) ;
Ps:如果有一个画笔的句柄,就可以通过调用GetObject取得LOGPEN结构各个成员的值:
GetObject (hPen, sizeof(LOGPEN), (LPVOID) &logpen) ;
如果需要当前选进设备描述表的画笔句柄,可以调用:
hPen = GetCurrentObject (hdc,OBJ_PEN) ;
实验
前面讲了这么多,我们练练手吧。
大家还记的我们在第二回见到的代码吗?那是我们所有程序的框架,以后我们所有的实验都要用到它。我们先把它搬到VC6.0中并把“case
WM_CREATE
”语段(行59-61)注释掉,目前我们不需要它。
一般我们把画图的代码放在WM_PAINT消息处理语句中接下来我们就要重写“case
WM_PAINT
”语段了,先画个矩形吧:
case WM_PAINT:
HDC hDC;
PAINTSTRUCT ps;
hDC=BeginPaint(hwnd,&ps);
Rectangle(hDC,100,,100,400,400);
EndPaint(hwnd,&ps);
break;
运行一下,大家看到矩形框了吧,同理大家再试一下Ellipse、RoundRect等这一系列的其他五个函数吧。(大家试着保持参数不变,看一下这一系列函数是不是共用一个矩形框)
我们接着在使用一下画笔吧,还记得使用自己创建的画笔的必要步骤么:创建、选入设备描述表、删除。
case WM_PAINT:
HDC hDC;
PAINTSTRUCT ps;
HPEN hPen;
hDC=BeginPaint(hwnd,&ps);
LOGPEN logpen;
logpen.lopnStyle=PS_SOLID;//设置线型为实线
logpen.lopnWidth.x=3;//设置现款为3像素
logpen.lopnColor=RGB(255,0,0);//设直线条颜色为红色
hPen=CreatePenIndirect(&logpen);
SelectObject(hDC,hPen);
Rectangle(hDC,100,100,300,300);
EndPaint(hwnd,&ps);
DeleteObject(hPen); //不要忘了删除画笔呀
break;
大家可以自行改一下线型、宽度、颜色再画一下其他图案吧,最后记得一定要试一下CretePen创建画笔,它其实更简单更常用。
最后我们做一点挑战东西来结束回会吧,我们就来绘制一个周期的正弦曲线,我们应该用什么函数来着?
请先在开头即“#include <windows.h>”下面加上以下几行:
#include <math.h>//我们要用正弦函数,所以要引用数学函数库
#define TWOPI (2*3.14159)//定义TWOPI(即2π)为(2*3.14159)
重写“case
WM_CREATE
”语段:
case WM_PAINT:
HDC hDC;
PAINTSTRUCT ps;
hDC=BeginPaint(hwnd,&ps);
int i ;
POINTapt [1000] ;
for (i = 0 ; i <1000 ; i++)
{
apt[i].x = i * 500 / 1000 ;
apt[i].y = (int) (500 / 2 * (1- sin (TWOPI *i / NUM))) ;//客户区//坐标原点在左上角,x轴y轴向右向下递增,这与平时数学坐标//不太一样
}
Polyline (hDC, apt, NUM) ;
EndPaint(hwnd,&ps);
break;
本程序有一个含有1000个POINT结构的数组。随着for循环从0增加到999,结构的x成员设定为从0递增到数值500。结构的y成员设定为一个周期的正弦曲线值,并被放大以填满500×500的区域。整个曲线的绘制仅仅使用了一个Polyline调用。
注逻辑画笔是一种“GDI对象”,它是您可以建立的六种GDI对象之一,其它五种是画刷、位图、区域、字体和调色板。除了调色板之外,这些对象都是通过SelectObject选进设备描述表的。
Ps:在使用画笔等GDI对象时,应该遵守以下三条规则:
- 最后要删除自己建立的所有GDI对象。
- 当GDI对象正在一个有效的设备描述表中使用时,不要删除它。
- 不要删除现有对象。(如画笔中的WHITE_PEN、BLACK_PEN和NULL_PEN)
①COLORREF类型用来描绘一个RGB颜色。其定义如下:
typedef DWORD COLORREF;
typedef DWORD *LPCOLORREF;
COLORREF类型变量值描绘一个颜色时对应于下面16进制的格式:
0x00bbggrr
可以用这样一个结构体来描述。
RGB_value struct
{
byte unused ;
byte blue ;
byte green ;
byte red;
};
其中第一字节为 0 而且始终为 0,其它三个字节分别表示兰色、绿色和红色,刚好和 RGB 的次序相反。这个结构体用起来挺别扭。对于COLORREF,我们通常使用宏RGB对其进行赋值。
宏的定义如下:
COLORREF RGB
(
BYTEbyRed, // red component of color
BYTEbyGreen, // green component of color
BYTEbyBlue // blue component of color
);
COLORREF 是一个 32-bit 整型数值,它代表了一种颜色。你可以使用 RGB 函数来初始化 COLORREF。例如:
COLORREF color=RGB(0,255,0);
RGB函数接收三个 0-255 数值,一个代表红色,一个代表绿色,一个代表蓝色。在上面的例子中,红色和蓝色值都为 0,所以在该颜色中没有红色和蓝色。绿色为最大值255。所以该颜色为绿色。0,0,0 为黑色,255,255,255为白色。
②POINT(点)是一个结构,它定义了一个点的坐标(x,y)
结构的定义如下:
typedef struct tagPOINT{
LONG x;
LONG y;
}POINT;
参数:
x: 指出一个点的x坐标.
y: 指出一个点的y坐标.
③如果你需要当前位置,就可以通过以下调用获得:
GetCurrentPositionEx (hdc, &pt) ;
其中,pt是POINT结构的。
④ iPenStyle 指定画笔样式,可以是下述标识符之一