常见的图形编程库,除了 GDI 外还有 GDI+、OpenGL、DirectX等等,GDI 是其中最基础的一个库。所以 GDI 注定了不会有高级应用,有兴趣的就当刷低级怪吧。
在教程的最开始,需要简单的说明一些前置条件。
开发环境与前言
首先是标明开发环境:
操作系统:win7 (xp应该可以,win8未测试)
使用工具:visual studio 2010(或更高)
窗口创建
以前代码的前置问题,首先本教程内的 GDI 画图,在最开始部分主要是在窗口内部绘制(为避免混乱窗口外部,也就是整个桌面的绘制会在很后面的地方讨论)。因此,这里需要对于创建窗口一定的了解。为了让大家可以直接复制完代码就可以在一个文件里面运行,博主准备的代码是手动动态创建窗口的代码,所以这里创建窗口的代码有点长,不过大家不要怕,我们要关注的只是中间的一小部分。这里博主先把代码贴上:
#include
// 用于注册的窗口类名
const char g_szClassName[] = "myWindowClass";
/*
* 第四步,窗口过程
*/
LRESULT CALLBACK MyWindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch(msg)
{
// 窗口绘制消息
case WM_PAINT:
/*
* 我们只需要在这里调用我们的 GDI 绘制函数就可以,其他地方可以先无视
*/
break;
// 窗口关闭消息
case WM_CLOSE:
DestroyWindow(hwnd);
break;
// 窗口销毁消息
case WM_DESTROY:
PostQuitMessage(0); // 发送离开消息给系统
break;
// 其他消息
default:
// pass 给系统,咱不管
return DefWindowProc(hwnd, msg, wParam, lParam);
}
return 0;
}
/*
* 第一步,注册窗口类
*/
void RegisterMyWindow(HINSTANCE hInstance)
{
WNDCLASSEX wc;
// 1)配置窗口属性
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = 0;
wc.lpfnWndProc = MyWindowProc; // 设置第四步的窗口过程回调函数
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
wc.lpszMenuName = NULL;
wc.lpszClassName = g_szClassName;
wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
// 2)注册
if(!RegisterClassEx(&wc))
{
MessageBox(NULL, TEXT("窗口注册失败!"), TEXT("错误"), MB_ICONEXCLAMATION | MB_OK);
exit(0); // 进程退出
}
}
/*
* 第二步,创建窗口
*/
HWND CreateMyWindow(HINSTANCE hInstance, int nCmdShow)
{
HWND hwnd;
hwnd = CreateWindowEx(
WS_EX_CLIENTEDGE,
g_szClassName,
TEXT("我的窗口名称"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 400, 300, // 出现坐标 x,y 默认分配 窗口宽 400 高 300
NULL, NULL, hInstance, NULL);
if(hwnd == NULL)
{
MessageBox(NULL, TEXT("窗口创建失败!"), TEXT("错误"), MB_ICONEXCLAMATION | MB_OK);
exit(0); // 进程退出
}
// 显示窗口
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);
return hwnd;
}
/*
* 主函数
*/
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow)
{
HWND hwnd;
MSG Msg;
// 第一步,注册窗口类
RegisterMyWindow(hInstance);
// 第二步:创建窗口
hwnd = CreateMyWindow(hInstance, nCmdShow);
// 第三步:消息循环
while(GetMessage(&Msg, NULL, 0, 0) > 0)
{
TranslateMessage(&Msg);
DispatchMessage(&Msg);
}
return Msg.wParam;
}
运行效果图:
创建一个空白窗口
这个创建窗口的代码很长,看起来有点吓人,但是学习 GDI 的过程中,这其中几乎是完全不需要记忆的,只要有一定的了解,然后会 copy 就可以了。当然如果你能懂也是更好,以上代码的出处为 《Windows SDK 教程(三) 一些细节以及动态创建控件》,有兴趣的可以去看看。
那么博主也说过了,最开始的时候,这段长长的代码其实注意一个地方就可以了,就是其中的第四步窗口过程中的一个小 case。
/*
* 第四步,窗口过程
*/
LRESULT CALLBACK MyWindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch(msg)
{
// 窗口绘制消息
case WM_PAINT:
/*
* 只有这一个 case 是我们 GDI 入门中需要注意的
*
* 当程序执行到这个地方的时候,意味着系统像我们的程序发送了 WM_PAINT 消息
* 也就是告诉我们的程序,可以开始绘制窗口的内容了。
*
*/
break;
// 其余略...
}
return 0;
}
这样看来,貌似我们要注意的地方确实很小吧。那么我接着往下走。
PS:默认情况下,系统只会向我们的程序发送一次 WM_PAINT 消息。如果想要再来一次,需要使用 SendMessage 函数,来自己向自己手动发送该消息。
坐标系
GDI 的绘图坐标系与普通的数学坐标系不同,原点 (0,0) 位于左上角。如图:
设备上下文(DC)
DC (设备上下文, Device Contexts)是 GDI 编程中一个很基础同时也很重要的概念。博主以前看过不少网上的资料以及书上的描述,总感觉他们说的都很奇怪。这里博主为了方便大家理解就说说自己的看法:
大家只要把 DC 当成一个保存图像的内存对象即可。当我们使用 GDI 提供的函数去操作 DC 的时候,也就意味着在使用函数去修改保存在这块内存上的图像。
BeginPaint 与 EndPaint
用于从目标窗口获取可画图的 DC,以及关闭这个 DC。
函数原型
HDC BeginPaint(
_In_ HWND hwnd, // 传入想要获取 DC 的窗口句柄
_Out_ LPPAINTSTRUCT lpPaint // 保存目标窗口的绘图信息
);
BOOL EndPaint(
_In_ HWND hWnd, // 目标窗口的句柄
_In_ const PAINTSTRUCT *lpPaint // 目标窗口的绘图信息
);
SelectObject
设置目标 DC 选中指定的对象(如画笔、画刷、图片等等)。
函数原型
HGDIOBJ SelectObject(
_In_ HDC hdc, // 目标 DC 的句柄
_In_ HGDIOBJ hgdiobj // 被选中的对象
);
CreatePen
创建一个画笔(pen)对象。
函数原型
HPEN CreatePen(
_In_ int fnPenStyle, // 样式
_In_ int nWidth, // 宽度
_In_ COLORREF crColor // 颜色
);
MoveToEx
移动绘制的初始位置。未移动则默认是 (0,0)。(C语言基础好的可以联想 fseek 函数)
函数原型
BOOL MoveToEx(
_In_ HDC hdc, // 操作目标DC的句柄
_In_ int X, // x 坐标
_In_ int Y, // y 坐标
_Out_ LPPOINT lpPoint // 保存移动后的当前坐标
);
LineTo
使用当前选中的对象(selected object、通常是画笔)从当前位置绘制一条直线到目标位置。
函数原型
BOOL LineTo(
_In_ HDC hdc, // 目标DC句柄
_In_ int nXEnd, // 目标位置 x 坐标
_In_ int nYEnd // 目标位置 y 坐标
);
绘制直线实例
#include
// 用于注册的窗口类名
const char g_szClassName[] = "myWindowClass";
void Paint(HWND hwnd)
{
// paint struct 绘图结构体,存储目标窗口可以绘图的客户端区域(client area)
PAINTSTRUCT ps;
HDC hdc; // DC(可画图的内存对象) 的句柄
HPEN hpen; // 画笔
// 通过窗口句柄获取该窗口的 DC
hdc = BeginPaint(hwnd, &ps);
// 创建画笔
hpen = CreatePen(PS_SOLID, 1, RGB(255,0,0));
// DC 选择画笔
SelectObject(hdc,hpen);
// (画笔)从初始点移动到 50,50
MoveToEx(hdc, 50, 50, NULL);
// (画笔)从初始点画线到 100,100
LineTo(hdc, 150, 100);
EndPaint(hwnd, &ps);
}
/*
* 第四步,窗口过程
*/
LRESULT CALLBACK MyWindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch(msg)
{
// 窗口绘制消息
case WM_PAINT:
Paint(hwnd); // 调用我们的 GDI 绘制函数
break;
// 其余略...
}
return 0;
}
// 其余略