GDI绘图
GDI(Graphics Device Interface)即Windows操作系统中的图形设备接口,它提供了一组用于在屏幕和打印机等图形设备上进行绘图的函数和工具。GDI绘图通常涉及使用GDI函数来创建、管理和绘制图形对象,如图形路径、画笔、刷子、字体等。通过使用这些图形对象,开发人员可以在应用程序中实现各种图形操作。
需要注意的是,早期GDI完全使用CPU在内存中绘制图像,它的绘制性能其实不高,随着Windows操作系统的发展,Windows7版本后优化了GDI的底层实现,部分函数如BitBlts、AlphaBlend等API支持了GPU加速绘制,然而这与Direct2D等完全基于GPU的绘制接口还是有区别的,后者能提供更现代、高性能的图形绘制能力,对于图形应用程序,开发人员可能更倾向于使用这些新的图形接口而不是传统的GDI。不过这里我们还是出于对Windows操作系统厚重历史的尊重,简单了解一下GDI的使用。
下面例子中,我们都将GDI操作抽取到一个单独的Draw函数,它会在触发WM_PAINT消息时回调。
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_PAINT:
Draw(hwnd);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
return DefWindowProc(hwnd, message, wParam, lParam);
}
}
基本概念
DC:DC(Device Context)即设备上下文,是一个对绘图设备的抽象,这里的绘图设备可以指显示器或打印机,甚至也可以是一段内存。
HDC:即DC的句柄,我们绘制任何内容其实都需要传入HDC。
PAINTSTRUCT:是Windows消息处理中的一个结构,用于处理WM_PAINT消息时,向应用程序提供绘图信息。其中包含了关于绘图区域的信息,如矩形区域的坐标等。
一个绘制流程具体到代码中,我们需要先通过BeginPaint函数获取HDC,进行一系列的绘制操作后,使用EndPaint结束绘制。
绘制点
GDI中,可以使用SetPixel绘制点。
void Draw(HWND hwnd)
{
HDC hdc;
PAINTSTRUCT ps;
hdc = BeginPaint(hwnd, &ps);
SetPixel(hdc, 100, 100, RGB(255, 0, 0));
EndPaint(hwnd, &ps);
}
SetPixel绘制单个点的性能是说得过去的,但如果要绘制直线、图形,不要使用大量的SetPixel操作。
绘制线
画线需要使用MoveToEx和LineTo两个函数,前者指定线段绘制的起始位置,后者指定线段绘制的结束位置。
void Draw(HWND hwnd)
{
HDC hdc;
PAINTSTRUCT ps;
hdc = BeginPaint(hwnd, &ps);
MoveToEx(hdc, 50, 50, NULL);
LineTo(hdc, 200, 200);
EndPaint(hwnd, &ps);
}
LineTo可以连续调用,绘制多条直线。但要注意,这里绘制的直线无论如何也不会被判定为封闭图形,无法填充颜色。如果需要绘制封闭图形,可以通过多边形或路径来绘制。
绘制图形
GDI提供了绘制圆形、矩形、圆角矩形等图形的函数,这些图形通常还具有一个填充颜色,这需要用到HBRUSH,下面是一个绘制红色圆形的例子。
void Draw(HWND hwnd)
{
HDC hdc;
PAINTSTRUCT ps;
hdc = BeginPaint(hwnd, &ps);
HBRUSH hRedBrush = CreateSolidBrush(RGB(255, 0, 0));
HBRUSH hOldBrush = (HBRUSH)SelectObject(hdc, hRedBrush);
Ellipse(hdc, 100, 100, 300, 300);
SelectObject(hdc, hOldBrush);
DeleteObject(hRedBrush);
EndPaint(hwnd, &ps);
}
代码中,我们创建HBRUSH后,调用了一个SelectObject函数,它用于将HBRUSH应用到DC上,而其返回值是DC上旧的HBRUSH。一个最佳实践是在绘制完成后,将对DC的任何更改都还原,然后销毁新创建的东西,大多数绘图框架都要求进行类似的操作,这很容易理解。因此在代码中结束绘制之前,我们分别调用了SelectObject和DeleteObject两个函数执行这些操作。
绘制多边形
下面例子演示了如何绘制自定义多边形,这需要我们自定义一些顶点,然后用Polygon函数绘制。
void Draw(HWND hwnd)
{
HDC hdc;
PAINTSTRUCT ps;
hdc = BeginPaint(hwnd, &ps);
HBRUSH hRedBrush = CreateSolidBrush(RGB(0, 255, 0));
HBRUSH hOldBrush = (HBRUSH)SelectObject(hdc, hRedBrush);
POINT points[] = {
{100, 100},
{200, 200},
{300, 100}
};
Polygon(hdc, points, 3);
SelectObject(hdc, hOldBrush);
DeleteObject(hRedBrush);
EndPaint(hwnd, &ps);
}
绘制文字
前面章节我们曾使用过DrawText函数绘制文字,但该函数的功能太过简单了,实际开发中一般使用TextOut函数,它的功能更加强大。绘制文字可能涉及字体、字号等众多信息,GDI中我们需要先加载字体资源并获取资源句柄,然后调用TextOut函数绘制文字。
void Draw(HWND hwnd)
{
HDC hdc;
PAINTSTRUCT ps;
hdc = BeginPaint(hwnd, &ps);
HFONT hFont = CreateFont(
40, // 字体高度
0, // 字体宽度
0, // 旋转角度
0, // 倾斜角度
FW_NORMAL, // 字体粗细
FALSE, // 斜体
FALSE, // 下划线
FALSE, // 删除线
DEFAULT_CHARSET, // 字符集
OUT_DEFAULT_PRECIS, // 输出精度
CLIP_DEFAULT_PRECIS, // 裁剪精度
DEFAULT_QUALITY, // 字体质量
DEFAULT_PITCH | FF_SWISS, // 字体族和字体类型
TEXT("微软雅黑") // 字体名称
);
HFONT hOldFont = (HFONT)SelectObject(hdc, hFont);
TextOut(hdc, 100, 100, TEXT("你好世界"), wcslen(TEXT("你好世界")));
SelectObject(hdc, hOldFont);
DeleteObject(hFont);
EndPaint(hwnd, &ps);
}
绘制位图
绘制位图和绘制其它简单图像有一些区别,我们需要先创建一个内存DC,然后在内存DC上绘制位图,最后再将内存DC中的内容绘制到窗口DC上。这其实是一种离线绘制方式,这样做是出于减少闪烁、保持原子性操作的目的,游戏开发中的双缓冲概念也是类似的。
void Draw(HWND hwnd)
{
HDC hdc;
PAINTSTRUCT ps;
hdc = BeginPaint(hwnd, &ps);
// 加载位图资源
HBITMAP hBitmap = LoadBitmap(GetModuleHandle(NULL), MAKEINTRESOURCE(IDB_BITMAP1));
// 创建内存DC用于绘制
HDC memDC = CreateCompatibleDC(hdc);
// 将位图绘制到内存DC
HBITMAP hOldBitmap = (HBITMAP)SelectObject(memDC, hBitmap);
// 获取位图的宽高
BITMAP bmp;
GetObject(hBitmap, sizeof(BITMAP), &bmp);
// 将内存DC中的内容绘制到窗口
BitBlt(hdc, 0, 0, bmp.bmWidth, bmp.bmHeight, memDC, 0, 0, SRCCOPY);
// 恢复内存DC
SelectObject(memDC, hOldBitmap);
// 释放内存DC
DeleteDC(memDC);
// 释放位图资源
DeleteObject(hBitmap);
EndPaint(hwnd, &ps);
}