WIN32 绘图新手指南






4.90/5 (58投票s)
初学者指南,了解如何在 WIN32 SDK 环境中绘制到窗口

引言
在阅读论坛并回答问题时,我发现许多初学者希望学习 WIN32 程序中绘图的基本概念。为 Windows 开发应用程序可能充满挑战和挫折。但如果你掌握了一些利用 WIN32 操作系统所需的基本技术,它也能带来丰厚的回报。本教程面向初学者,将涵盖开始 WIN32 绘图所需的基本技术和知识。
代码和概念都处于 SDK 级别,因为大多数用于 Windows 绘图的其他技术都基于 SDK。WIN32 SDK 用于 MFC 和 ATL/WTL 对象内部,这些对象代表了这些框架。我相信你对基础技术(SDK)了解得越多,就越能充分利用那些旨在封装该技术的框架。为了帮助使用 MFC 和 WTL 进行开发的开发人员,将在适当的部分解释这些框架中对应的类。
本教程是五个系列中的第一个。本教程面向初学者,接下来的三个教程将涵盖中级更高级的主题,而最后一个教程将涵盖高级 WIN32 绘图内部原理。
设备上下文
从最简单的层面来看,设备上下文(DC)是一个可以进行绘图的表面。然而,DC 也是操作系统资源,它协调用于在显示器上渲染图像的画刷、画笔和字体。它也是 Windows 图形设备接口(GDI)用于将显示设备的细节从开发人员那里抽象出来的抽象层。这意味着可以使用相同的代码在屏幕、打印机,甚至内存中的位图上进行绘图,只需使用为特定目的创建的不同 DC 进行初始化即可。与直接为特定显卡或打印机驱动程序编程所需的开发相比,这使得编码非常简单。
在 Windows 中创建稳健高效图形的关键是了解如何创建和利用你为特定目的所需的 DC。DC 有很多种,下面简要介绍一下每种类型:
客户端 DC
客户端 DC 与特定窗口关联,并将允许开发人员访问目标窗口的客户区。这是应用程序程序员最常使用的 DC 类型,也是最容易处理的。这是处理
WM_PAINT
消息时应使用的 DC 类型。这也是唯一将详细解释的 DC。窗口 DC
窗口 DC 与特定窗口关联,允许开发人员在目标窗口的任何部分进行绘制,包括边框和标题栏。这是与
WM_NCPAINT
消息一起发送的 DC 类型。内存 DC
内存 DC 是仅存在于内存中,不与任何窗口关联的 DC。这是唯一可以保存用户定义位图 (
HBITMAP
) 的 DC 类型。内存 DC 对于缓存图像以及在复杂显示器上用作后台缓冲区非常有用。通用设备 DC
由于没有更好的名称,这种类型的 DC 涵盖了所有其他可以从中获取 DC 的设备。例如,打印机或绘图仪、整个显示器,甚至某个尖端科技公司可能发明但尚未存在的定制设备。事实是,可以为任何支持 Microsoft 定义的所需 API 函数的设备创建 DC。
本指南将只演示客户端 DC,以帮助用户开始基本的 Windows 图形开发。本系列后续教程将介绍其他类型的 DC。
获取客户端设备上下文
在使用 WIN32 绘图 API 时,你将始终获取设备上下文的句柄 (HDC
)。前面描述的任何类型的 DC 都可以存储在 HDC 中。本指南将只描述如何获取客户端 DC,因为其他 DC 用于更高级的用途。
框架用类表示它们的 DC。DC 的基类是 CDC
。CDC
封装了 HDC 本身。可以为 HDC 调用的所有函数都作为成员函数封装起来。还有一些从 CDC
派生的类,它们允许创建和维护特殊的 DC。CPaintDC
和 CClientDC
将在本节后面解释。
BeginPaint
BeginPaint
是创建 DC 最常见的方式。但是,此函数应该只在窗口的 WM_PAINT
消息处理程序中调用。这是因为 BeginPaint
会验证窗口的无效区域。每当窗口上创建无效区域时,都会生成 WM_PAINT
消息。如果在 WM_PAINT
处理程序之外调用 BeginPaint
,任何先前无效的区域都将被验证,并且不会为窗口生成 WM_PAINT
消息。这可能会导致控件出现严重的副作用,特别是如果将来有人希望子类化你的控件。
在 WM_PAINT
消息处理程序中使用 BeginPaint
与不在 WM_PAINT
消息处理程序之外使用它一样重要。这是因为在 BeginPaint
函数调用内部,如果需要,Windows 可能会生成 WM_ERASEBKGND
消息和 WM_NCPAINT
消息。如果你不在 WM_PAINT
消息处理程序内部调用 BeginPaint
,你的窗口边框可能无法正确更新。
为了通过 BeginPaint
获取 DC 的句柄,你需要目标窗口的句柄和一个 PAINTSTRUCT
结构体。PAINTSTRUCT
在你调用 BeginPaint
时保存当前绘图会话的信息,这在这个级别并不重要。BeginPaint
还返回它创建的 DC 的句柄,这是我们感兴趣的值。下面是一个如何调用 BeginPaint
的例子。
// assuming that hWnd is the handle to the window for which we want the DC.
PAINTSTRUCT ps;
HDC hdc;
hdc = ::BeginPaint(hWnd, &ps);
用于释放通过 BeginPaint
创建的 HDC 的函数是 EndPaint
。调用 EndPaint
而不是其他 DC 销毁器很重要,因为 BeginPaint
调用 HideCursor
以防止在绘图操作期间绘制光标,而 EndPaint
调用 ShowCursor
使其再次可见。如果 BeginPaint
之后没有调用 EndPaint
,你可能会遇到一些奇怪的光标异常。
这是 EndPaint
的一个例子
// Call EndPaint with the same hWnd and PAINTSTRUCT that was used in
// the call to BeginPaint.
::EndPaint(hWnd, &ps);
MFC 和 WTL 中的 CPaintDC
类封装了对 BeginPaint
和 EndPaint
的调用。只需在堆栈上创建此 DC 的实例,DC 就会自动为开发人员创建和销毁。以下是此类的 MFC 版本中构造函数和析构函数的代码。
CPaintDC::CPaintDC(CWnd* pWnd)
{
...
if (!Attach(::BeginPaint(m_hWnd = pWnd->m_hWnd, &m_ps)))
AfxThrowResourceException();
}
CPaintDC::~CPaintDC()
{
...
::EndPaint(m_hWnd, &m_ps);
Detach();
}
GetDC / GetDCEx
GetDC
是创建 DC 的第二种最常见方式。GetDC
将获取目标窗口客户区的设备上下文。GetDCEx
将做同样的事情,但它允许你指定一个默认的剪裁区域。GetDCEx
将在下一次指南中忽略。
GetDC
在 WM_PAINT
消息处理程序之外有许多用途。当需要创建图形效果,但该效果可能不是目标窗口永久数据的一部分时,请使用 GetDC
。例如,在绘图工具上创建橡皮筋效果以及在窗口中选择多个对象时,请使用 GetDC
来创建 DC。
以下是使用 GetDC
调用创建和销毁 DC 的示例。
// Assuming that hWnd is the handle to the window for which we want the DC.
HDC hdc;
hdc = GetDC(hWnd);
// Perform painting operations here.
...
// Release the DC when you are finished. If this function succeeds it will return 1,
// otherwise it will return 0 if it fails.
::ReleaseDC(hWnd, hdc);
MFC 和 WTL 中的 CClientDC
类封装了对 GetDC
和 ReleaseDC
的调用。只需在堆栈上创建此 DC 的实例,DC 就会自动为开发人员创建和销毁。以下是此类的 MFC 版本中构造函数和析构函数的代码。
CClientDC::CClientDC(CWnd* pWnd)
{
...
if (!Attach(::GetDC(m_hWnd = pWnd->GetSafeHwnd())))
AfxThrowResourceException();
}
CClientDC::~CClientDC()
{
...
::ReleaseDC(m_hWnd, Detach());
}
使用设备上下文
使用 DC 非常简单,也可以非常复杂,这完全取决于要实现什么样的绘图效果。本指南将只使用 DC 初次创建时选定的默认画笔和画刷。下面是一个如何使用我们创建的 DC 调用许多不同 GDI 函数的示例。
// Draw a rectangle at (100,100) with dimensions (100,200);
Rectangle(hdc, 100, 100, 200, 300);
// Draw an ellipse inside the previous rectangle.
Ellipse(hdc, 100, 100, 200, 300);
// Draw simple text string on the window.
TCHAR szMessage[] = "Paint Beginner";
UINT nLen = _tcslen(szMessage);
TextOut(hdc, 100, 325, szMessage, nLen);
以下是演示如何使用 CPaintDC
的简短示例
//C: Create the DC on the stack. This will allow the class to be destroyed when
// the stack frame disappears.
//C: WARNING: Only use this DC in your OnPaint handler for the WM_PAINT message.
CPaintDC dc;
//C: Use the DC as you would like.
dc.Rectangle(10, 10, 150, 200);
...
//C: No need to do any thing else to manage the DC, it will destroy itself.
以下是演示如何使用 CClientDC
的简短示例
//C: Create the DC on the stack. This will allow the class to be destroyed when
// the stack frame disappears.
CClientDC dc;
//C: Use the DC as you would like.
dc.Rectangle(10, 10, 150, 200);
...
//C: No need to do any thing else to manage the DC, it will destroy itself.
入门
提供的简单演示程序将显示用户创建的一系列形状。用户可以通过单击并拖动鼠标来创建形状,就像在 Windows 资源管理器中选择对象一样。演示了创建客户端 DC 的两种方法。
BeginPaint
:此函数用于绘制用户创建的形状。GetDC
:此函数用于创建橡皮筋效果,允许用户拖动并创建形状。
演示程序结构非常简单。它为了简洁明了而放弃了编码风格和优雅。该程序所需的所有状态都存储在全局变量中。最多可以创建 5 个形状,因为它们存储在静态分配的数组中。如果创建了超过 5 个形状,则最旧的现有形状将被新形状替换。
这是为处理 WM_PAINT
消息而创建的 OnPaint 处理程序
LRESULT OnPaint (HWND hWnd)
{
PAINTSTRUCT ps;
HDC hdc;
hdc = ::BeginPaint(hWnd, &ps);
UINT index;
for (index = 0; index < SHAPE_COUNT; index++)
{
if (ID_SHAPE_RECTANGLE == Shapes[index].shapeID)
{
::Rectangle (
hdc,
Shapes[index].rect.left,
Shapes[index].rect.top,
Shapes[index].rect.right,
Shapes[index].rect.bottom
);
}
else
{
::Ellipse (
hdc,
Shapes[index].rect.left,
Shapes[index].rect.top,
Shapes[index].rect.right,
Shapes[index].rect.bottom
);
}
}
::EndPaint(hWnd, &ps);
return 0;
}
请注意 BeginPaint
和 EndPaint
调用之间封装的非常简单的绘图结构。在 BeginPaint
括号之间可以进行更多的调用。同样的原则仍然适用,主窗口的所有绘图都应该在这里完成。
橡皮筋效果稍微复杂一些。这种效果是通过修改 DC 中的两个状态变量来实现的。第一个改变的状态是当前绘图模式从简单的复制变为目标 NOT 画笔,即 R2_NOT
。这种画笔将允许画笔的第一次绘图实例改变显示中的所有位,使线条可见。通过简单地第二次绘制完全相同的线条,线条就会消失。
DC 状态的第二个改变是选择一个空画刷颜色到 DC 中,这样当拖动形状时,它不会绘制形状的中心。这两个技巧不是这段代码中应该注意的重点。需要注意的重点是 DC 是从 GetDC
调用接收的,并以 ReleaseDC
调用结束。与绘图代码相同,任何操作都包含在此 DC 创建括号中,并且窗口绘图在此之间进行。
这是绘制橡皮筋效果的函数
void DrawRubberBand(HWND hWnd)
{
HDC hdc;
//C: Get a client DC.
hdc = ::GetDC(hWnd);
//C: Set the current drawing mode to XOR, this will allow us
// to add the rubber band, and later remove it by sending the
// exact same drawing command.
::SetROP2(hdc, R2_NOT);
//C: Select a NULL Brush into the DC so that no fill is performed.
::SelectObject(hdc, ::GetStockObject(NULL_BRUSH));
//C: Get the current shape mode.
HMENU hMenu = ::GetMenu(hWnd);
HMENU hShapeMenu = ::GetSubMenu(hMenu, 1);
if (::GetMenuState(hShapeMenu, ID_SHAPE_RECTANGLE, MF_BYCOMMAND) & MF_CHECKED)
{
::Rectangle(
hdc,
ptStart.x,
ptStart.y,
ptCurrent.x,
ptCurrent.y
);
}
else
{
::Ellipse(
hdc,
ptStart.x,
ptStart.y,
ptCurrent.x,
ptCurrent.y
);
}
//C: Release the DC.
::ReleaseDC(hWnd, hdc);
}
关于演示程序,最后一点需要解释的是使橡皮筋起作用的调用。这段代码在 WM_MOUSEMOVE
消息处理程序中。处理程序会首先检查橡皮筋当前是否已激活。如果是,它会先绘制当前矩形,有效地从屏幕上擦除它,然后更新当前坐标,再绘制新坐标,有效地更新矩形的位置。代码如下:
LRESULT OnMouseMove (HWND hWnd, UINT nCtrl, UINT x, UINT y)
{
//C: If the current mode is not rubber band, then exit.
if (!isRubberBand)
{
return 0;
}
//C: Undo the last rectangle shape that was drawn.
DrawRubberBand(hWnd);
//C: Update the current position.
ptCurrent.x = x;
ptCurrent.y = y;
//C: Draw the next rubberband position.
DrawRubberBand(hWnd);
//C: Exit with success.
return 0;
}
结论
再次强调,本指南旨在引导人们踏上 WIN32 绘图之路。本文完全是为 WIN32 SDK 编写的。我完全相信 MFC 和 ATL 是开发 Windows 代码更简洁的方式,但我也相信如果开发人员能够牢固掌握 SDK 级别的基本概念,他们将能够作为框架开发人员蓬勃发展,因为你将能够确定幕后发生的事情,尤其是在出现问题时。
我发布的下一篇文章将是中级水平,包含更多信息。下一篇文章将介绍创建 DC 的其他方法。还将提供关于 PAINTSTRUCT
结构不同字段的信息。最后,将详细解释更新区域。