常见的图形编程库,除了 GDI 外还有 GDI+、OpenGL、DirectX等等,GDI 是其中最基础的一个库。所以 GDI 注定了不会有高级应用,有兴趣的就当刷低级怪吧。

在教程的最开始,需要简单的说明一些前置条件。

开发环境与前言

首先是标明开发环境:
操作系统:win7 (xp应该可以,win8未测试)
使用工具:visual studio 2010(或更高)

窗口创建

以前代码的前置问题,首先本教程内的 GDI 画图,在最开始部分主要是在窗口内部绘制(为避免混乱窗口外部,也就是整个桌面的绘制会在很后面的地方讨论)。因此,这里需要对于创建窗口一定的了解。为了让大家可以直接复制完代码就可以在一个文件里面运行,博主准备的代码是手动动态创建窗口的代码,所以这里创建窗口的代码有点长,不过大家不要怕,我们要关注的只是中间的一小部分。这里博主先把代码贴上:

#include <windows.h>

// 用于注册的窗口类名
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;
}

运行效果图:

Windows GDI 实现 一个简单的绘图程序_社会时事

这个创建窗口的代码很长,看起来有点吓人,但是学习 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) 位于左上角。如图:

Windows GDI 实现 一个简单的绘图程序_社会时事_02

设备上下文(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 <windows.h>

// 用于注册的窗口类名
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;
}

// 其余略

运行效果图:
Windows GDI 实现 一个简单的绘图程序_社会时事_03