窗口大小?绘图区域大小?

以下是创建窗体的函数

1
2
3
4
5
6
7
HWND hwnd = CreateWindowEx(
NULL,
CLASSNAME, TITLE,
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
CW_USEDEFAULT, CW_USEDEFAULT, 640, 400,
NULL, NULL, hInstance, NULL
);

其中640, 480表示的是窗口大小,而不是绘图区域的大小。
所以当你画一条从(0,0)到(640,480)的线时,会发现画出的线并不是对角线。
这时候需要一个叫AdjustWindowRectEx的函数
函数原型
BOOL AdjustWindowRectEX(LPRECT lpRect, DWORD dwStyte, BOOL bMenu, DWORD dwExStyle);
函数功能
该函数依据所需客户矩形大小,计算需要的窗口矩形的大小。计算出的窗口矩形随后可以传送给CreateWindowEx函数,用于创建一个客户区所需大小的窗口。
lpRect:指向RECT结构的指针,该结构包含所需客户区域的左上角和右下角的坐标。函数返回时,该结构包含容纳所需客户区域的窗口的左上角和右下角的坐标。
dwStyle:指定将被计算尺寸的窗口的窗口风格。
bMenu:指示窗口是否有菜单。
dwExStyle:指定将被计算尺寸的窗口的扩展窗口风格。

所以创建窗体的代码,可以这样写:

1
2
3
4
5
6
7
8
9
RECT rect = { 0, 0, 640, 480 };
AdjustWindowRectEx(&rect, WS_OVERLAPPEDWINDOW | WS_VISIBLE, 0, 0);
HWND hwnd = CreateWindowEx(
NULL,
CLASSNAME, TITLE,
WS_OVERLAPPEDWINDOW | WS_VISIBLE,
CW_USEDEFAULT, CW_USEDEFAULT, rect.right-rect.left, rect.bottom-rect.top,
NULL, NULL, hInstance, NULL
);


画笔和画刷,绘制几何图形

由于WM_PAINT消息只有在少数情况下才会产生,所以在WM_PAINT里面绘图无法实现一些动画效果(连续地作图)
所以,我们可以在消息循环的while函数中绘图。
与在WndProc中的绘图不同,这时我们不能用beginpaint来获取设备描述表句柄。
我们采用的是 HDC hdc = GetDC(hwnd);
同时endpaint函数要改成 ReleaseDC(hwnd, hdc);
这样绘图还有些问题,之后会想办法解决。

1.画线

众所周知,一条线有三个信息:线类型、线宽、线颜色。
可以使用 HPEN CreatePen( int iStyle, int cWidth, COLORREF color) 获取这样的画笔的句柄
返回值:画笔的句柄:HPEN
值得注意的是,istyle是线的风格,cwidth是线的宽度
istyle=0时线是实线,istyle>1时线有各式各样的风格。
而只有当cwidth=1的时候(即线的宽度为1),istyle才能取>1的值。

当然,得到了这个画笔句柄还不够,还要把这个画笔选择到设备描述表(DC)中。
所以,需要使用 HGDIOBJ SelectObject( HDC hdc, HGDIOBJ h) 函数
其中,hdc是设备描述表句柄。h是要选择的句柄。
返回值是与该句柄类型相同的旧句柄。而该旧句柄已经被新句柄替换掉了。

选择该句柄之后,开始画线了。
画线需要两个函数:
MoveToEx(hdc, x1, y1, NULL); //将当前绘图位置移动到(x1,y1)。
LineTo(hdc, x2, y2); //用当前画笔画一条线,从当前绘图位置连到点(x2,y2)。
这两个函数表示,用画笔从(x1,y1)连线到(x2,y2)。

这样真的就结束了吗?
大家可以写一个程序。在消息循环中加入以下绘图代码:

1
2
3
4
5
6
HDC hdc = GetDC(hwnd);
HPEN hpen = CreatePen(1, 3, RGB(rand()%256,rand()%256,rand()%256));
SelectObject(hdc, hpen);
MoveToEx(hdc, 0, 0, NULL);
LineTo(hdc, 640, 480);
ReleaseDC(hwnd, hdc);

乍一看这代码没有问题,但是绘图一段时间之后,颜色不再变化。
我们查询一下百度百科,发现:
“当不再需要画笔后,要释放CreatePen创建出来的画笔(特别是在一直使用这个函数创建画笔的时候)的资源,用DeleteObject函数。”
于是我们尝试一下,在代码的最后面加上 DeleteObject(hpen)
奇迹般地发现,bug消失了。
正确代码

1
2
3
4
5
6
7
HDC hdc = GetDC(hwnd);
HPEN hpen = CreatePen(1, 3, RGB(rand()%256,rand()%256,rand()%256));
SelectObject(hdc, hpen);
MoveToEx(hdc, 0, 0, NULL);
LineTo(hdc, 640, 480);
DeleteObject(hpen);
ReleaseDC(hwnd, hdc);

2.画矩形

画矩形有两大元素,
其边框,其内部填充。
画边框可以用画笔hpen来画,之前讲过。
填充就需要画刷hbrush来画了。
最常用的是CreateSolidBrush函数来得到画刷句柄
HBRUSH CreateSolidBrush( COLORREF color);
创建一个颜色为color的画刷,返回该画刷句柄。
还有较为常用的是CreateHatchPattern函数
HBRUSH CreateHatchBrush( int iHatch, COLORREF color);
其中iHatch表明该画刷的风格。例如,iHatch=0的时候,返回的是水平阴影的画刷。

使用SelectObject,将hpen和hbrush选择到设备描述表中。

画矩形用Rectangle(hdc, x1, y1, x2, y2)函数
表示用画笔和画刷,画一个左上角为(x1,y1),右下角为(x2,y2)的矩形。

同样,在使用过该画刷之后,要用DeleteObject释放资源

可能这样功能还不齐全,
如果我们想画个没有边框的矩形?不填充的矩形?需要以下操作:
const HANDLE hnullbrush = GetStockObject(NULL_BRUSH);
const HANDLE hnullpen = GetStockObject(NULL_PEN);
GetStockObject函数可以检索预定义的备用笔、刷子、字体或者调色板的句柄,
GetStockObject(NULL_BRUSH)表示获得空画刷的句柄(即不填充)
GetStockObject(NULL_PEN)表示获得空画笔的句柄(即不画边框)
可以用 SelectObject(hnullbrush) 或 SelectObject(hnullpen)
来决定如何画这个矩形。

以下代码是绘制一个绿色的无边框矩形:

1
2
3
4
5
6
7
HDC hdc = GetDC(hwnd);
hbrush = CreateSolidBrush(RGB(0, 255, 0));
SelectObject(hdc, hbrush);
SelectObject(hdc, hnullpen);
Rectangle(hdc, 300, 100, 400, 200);
DeleteObject(hbrush);
ReleaseDC(hwnd, hdc);

3.画多边形

WinApi里面有许许多多的绘图函数,可是就是找不到triangle??
大概是因为 triangle 属于多边形的范畴^-^
我们可以用Polygon函数来画多边形
BOOL Polygon(HDC hdc, CONST POINT *lpPoints, int nCount);
该函数画由两个以上顶点组成的多边形,用当前画笔画多边形轮廓,用当前画刷和多边形填充模式填充多边形。
lpPoints:指向要画的多边形的顶点数组的指针
nCount: 指向要画的多边形的顶点数

以下代码是绘制一个三角形:

1
2
3
4
5
6
7
8
9
10
HDC hdc = GetDC(hwnd);
hbrush = CreateHatchBrush(0, RGB(255, 0, 0));
hpen = CreatePen(1, 1, RGB(0, 0, 0));
SelectObject(hdc, hbrush);
SelectObject(hdc, hpen);
POINT poly[3] = { { 100,100 },{ 600,200 },{ 200,300 } };
Polygon(hdc, &poly[0], 3);
DeleteObject(hbrush);
DeleteObject(hpen);
ReleaseDC(hwnd, hdc);

4.画椭圆

WinApi函数中没有出现circle???
大概是因为 circle 属于椭圆的范畴^-^
我们可以用Ellipse函数里画椭圆
BOOL Ellipse(HDC hdc, int x1, int y1, int x2, int y2);
若有一个(x1,y1)为左上角,(x2,y2)为右下角的矩形,该函数画的是内切于该矩形的椭圆。

以下代码是绘制一个椭圆:

1
2
3
4
5
6
7
HDC hdc = GetDC(hwnd);
hpen = CreatePen(0, 5, RGB(0, 0, 255));
SelectObject(hdc, hpen);
SelectObject(hdc, hnullbrush);
Ellipse(hdc, 350 - 50, 150 - 20, 350 + 50, 150 + 20);
DeleteObject(hpen);
ReleaseDC(hwnd, hdc);

5.画点!

最重要的画点函数怎么可以漏掉呢?!?!
一个很简单(但是…)的函数:Setpixel(x,y,color)
表示绘制像素点在坐标(x,y)上。颜色为color。

以下代码是用画点绘制一个比较奇怪的圆:

1
2
3
4
5
HDC hdc = GetDC(hwnd);
for(int i=-50; i<=50; ++i)
for (int j = -50; j <= 50; ++j)if ((i * i + j * j <= 50 * 50) && (((i>>1)^(j>>1)) & 1))
SetPixel(hdc, 50 + i, 300 + j, RGB(255, 0, 255));
ReleaseDC(hwnd, hdc);

可见所有的图案都可用Setpixel画出来。
然而,Setpixel太慢了!太慢了!太慢了!
一秒钟大概只能绘制几百万个点。而全屏窗口的像素点个数大概就有百万个了。
Setpixel的速度限制了帧数,限制了不能一口气画太多的点。
怎么办呢?以后会讲到解决方法。

6.其他绘图函数

RoundRect
Chord
Pie
Arc
PolyBezier

大家可以都实践一下^-^,这里不一一列举了


那么笔记到这里结束了!

完整代码运行结果:

截图
编程环境:VS2017
源代码下载:painting.zip