65.9K
CodeProject 正在变化。 阅读更多。
Home

WIN32 绘图新手指南

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (58投票s)

2002年3月21日

CPOL

10分钟阅读

viewsIcon

366586

downloadIcon

11890

初学者指南,了解如何在 WIN32 SDK 环境中绘制到窗口

Sample Image

引言

在阅读论坛并回答问题时,我发现许多初学者希望学习 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 的基类是 CDCCDC 封装了 HDC 本身。可以为 HDC 调用的所有函数都作为成员函数封装起来。还有一些从 CDC 派生的类,它们允许创建和维护特殊的 DC。CPaintDCCClientDC 将在本节后面解释。

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 类封装了对 BeginPaintEndPaint 的调用。只需在堆栈上创建此 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 将在下一次指南中忽略。

GetDCWM_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 类封装了对 GetDCReleaseDC 的调用。只需在堆栈上创建此 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;    
}

请注意 BeginPaintEndPaint 调用之间封装的非常简单的绘图结构。在 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 结构不同字段的信息。最后,将详细解释更新区域。

© . All rights reserved.