Windows 绘图 101






4.76/5 (13投票s)
Windows 中绘图的基本概念。
引言
Windows 中的绘图并不复杂,但需要理解一些概念。
- 所有绘图都在 DC (设备上下文) 中完成!
- 直接在窗口 DC(通过 GDI 访问显存)中绘图有时会产生闪烁,因此一种常用技术是创建一个内存 DC,在其中进行绘图,然后在需要时(在
WM_PAINT
消息中)将内存 DC 的图像 Bitblt(位块传输)到窗口 DC。这使得您的图像具有所谓的“持久性”。一旦图像绘制完成,就不需要再次重绘,除非它发生变化。您只需在WM_PAINT
消息中 Bitblt 图像即可在需要时刷新它。 - 使用
CreateCompatibleDC
函数创建内存 DC。然后,您必须使用CreateCompatibleBitmap
函数创建一个位图来存储图像。您必须使用SelectObject
函数将位图选入内存 DC。确保您保存SelectObject
的返回值,因为它是 DC 中原始位图的句柄(一个 1x1 像素的位图)。销毁 DC 时(使用DeleteDC
)(当不再需要时),您必须将旧位图选回内存 DC,然后删除您创建的位图(使用DeleteObject
)。 - 要在内存 DC 中绘图,您可以使用任何 GDI(图形设备接口)API 函数。您应该使用
CreatePen
函数定义画笔颜色。您应该使用CreateSolidbrush
函数定义画刷颜色。有许多绘图函数可用于绘制矩形、圆形、线条等(例如Rectangle
、Ellipse
、LineTo
和MoveToEx
函数)。创建的画笔和画刷必须在调用 GDI 函数进行绘图之前选入 DC。请记住保存SelectObject
函数的返回值,并在完成后将旧画笔和画刷选回 DC。
DC 本质上是内存中的一个区域(在 RAM 或显存中),Windows 可以在其中进行绘图。每个 DC 都有自己的属性,例如选定到 DC 中的字体、用于绘图的画笔颜色、用于填充大区域的画刷(一个小位图)等。除了这些,还有许多其他属性,例如剪裁区域、视口、映射模式等。如果您只关注像素操作,那么很多属性您不必担心。
Windows 绘图的关键概念
Windows 绘图需要理解许多基本概念。首先是所谓的 DC。
- DC(设备上下文)本质上是一个用于绘图的已定义区域,它拥有自己独特的属性集。有不同种类的 DC。一种是窗口 DC,这最终意味着绘制的区域是显卡显存,因此图像对用户可见。Windows 会屏蔽您对显存的直接访问(除非您使用 DirectX),但图像Nonetheless(无论如何)会在显存上绘制。第二种 DC 是内存 DC。程序员可以根据需要创建任何他们喜欢的内存 DC。绘制的区域将是已选入 DC 的位图。一种常用技术是创建一个内存 DC,将其选入一个位图,然后在上面绘图,一旦图像完成,就将该图像从内存 DC Bitblt(复制)到窗口 DC(使其可见)。另一种 DC 是打印机 DC,绘制的区域是打印页面。
- 处理
WM_PAINT
消息,并使用BeginPaint
API 函数获取 DC。现在您可以对其进行绘图,直到执行Endpaint
。 - 使用
GetDC
API 函数获取任何窗口的客户区(边框内部)的 DC。您可以在此 DC 上绘图,图像将立即出现在屏幕上。完成后,使用ReleaseDC
API 函数释放 DC。 - 最后,您可以使用
GetWindowDC
API 函数获取任何窗口的整个窗口的 DC。这包括非客户区,即控件的边框。同样,绘图完成后,您必须使用ReleaseDC
函数释放 DC。 - 每个 DC 都有一组属性。新创建的 DC 的属性设置为默认设置(例如,内存 DC)。共享 DC(窗口 DC 可以被许多不同的窗口共享)拥有 DC 上次访问时定义的属性。通常,在使用共享 DC(例如窗口 DC)时,您应该始终将 DC 的属性恢复到您开始访问 DC 之前的状态。这可以通过使用
SaveDC
和RestoreDC
函数来完成。
要访问窗口 DC,有三种方法
DC 的属性有哪些?
有很多,但我这里只列出一些常用的
Attribute | 描述 | 创建它的函数 | 删除它的函数 | 设置它的函数 |
---|---|---|---|---|
画笔 |
绘图的线条颜色、宽度和样式 | CreatePen |
DeleteObject |
SelectObject |
Brush |
用于填充形状的 8x8 像素图案 | CreateSolidBrush , CreateHatchBrush , CreatePatternBrush |
DeleteObject |
SelectObject |
字体 |
用于绘制文本的字体 | CreateFont |
DeleteObject |
SelectObject |
Text_FG_Color |
文本颜色 | SetTextColor |
||
Text_BG_Color |
绘制文本后面的背景颜色 | SetBKColor |
||
背景填充模式 | 背景填充模式 | SetBKMode (实心[填充]或透明) |
||
绘图模式 | 绘制的对象如何与现有背景图像混合 | SetROP2 |
||
. | . | . | . | . |
函数 | 描述 |
---|---|
MoveToEx |
将画笔位置移动到 X,Y 位置 |
SetPixel |
按颜色绘制像素并返回前一个颜色 |
SetPixelV |
按颜色绘制像素 |
椭圆 |
绘制一个椭圆 |
矩形 |
绘制一个矩形 |
LineTo |
从画笔位置绘制一条线到新位置 |
Arc |
绘制一个弧 |
PolyLineTo |
通过数组中的值绘制多条线 |
RoundRect |
绘制一个圆角矩形 |
Polygon |
绘制一个多边形 |
Pie |
绘制一个扇形对象 |
BitBlt |
将图像从一个 DC 复制到另一个 DC(位图) |
StretchBlt |
将图像从一个 DC 复制并拉伸到另一个 DC(位图) |
绘图的最佳方法之一是使用内存 DC 进行绘图,当您需要将其显示在屏幕上时,只需使用 BitBlt
函数在 WM_PAINT
消息期间将其从内存 DC 复制到窗口 DC。
如何创建内存 DC
hDC& = CreateCompatibleDC(%NULL) ' make DC compatible with screen
' newly created DC has a default 1 x 1 pixel monchrome bitmap selected in it
W& = 200 ' pixels
H& = 200 ' pixels
' if you want to make bitmap the size of a windows client area
' use the GetClientRect function to get width and height
hBmp& = CreateCompatibleBitmap(hDC&, W&, H&)
OriginalBitmap& = SelectObject(hDC&, hBmp&)
SaveDC hDC&
'
' now you can draw on this DC using any drawing command
' you can also BitBlt this image into a window DC during WM_PAINT
'
'
' when you are finished with the memory DC and no longer need it
' you must do the following:
SelectObject hDC&, OriginalBitmap&
DeleteObject hBmp&
RestoreDC hDC&, -1
' delete any still existing objects created like pens or brushes
DeleteDC hDC&
如何绘图
让我们开始真正的绘图吧!
Windows 在绘图时有很多开销。不像以前(还记得 Commodore 64 吗),那时您可以直接访问显存。特别是当您使用 SetPixel
等 API 函数时,您可以看到在 Windows 中绘图有多慢。API 命令绘制的区域越大,每像素的绘图速度就越快。作为测试,使用 Rectangle
API 函数绘制一个大的填充矩形,然后尝试使用 SetPixelV
(比 SetPixel
快)API 函数逐像素绘制相同的填充矩形。速度差异将是惊人的。两种技术都完成了相同的任务,但使用 SetPixel
绘图展示了 Windows 在绘图方面巨大的开销。
下一个问题是直接在 RAM 中绘图与直接在显存中绘图的区别。当您直接在 Windows DC(设备上下文)中绘图时,您实际上是在显存中绘图。与在普通 RAM 中绘图相比,显存绘图非常慢。
使 Windows 变慢的原因是设备上下文(DC)的安排。然而,使用 DC 有其目的。它允许 Windows 使用相同的 GDI 函数在各种设备上进行绘图,例如显存、普通 RAM、打印机或任何其他可以绘制的设备。DC 的使用非常强大,但缺点是需要大量的额外开销来跟踪与 DC 相关的所有事物。需要将对象选入和选出 DC 会增加显著的开销。Windows 需要 DC 来完成它的工作。它只是减慢了速度。
现在,利用上述信息,我们可以开发更好的 Windows 绘图方法以获得更快的显示速率。以下是一些加速技术。
- 在 RAM 中绘图比在显存(Windows DC)中绘图要快得多!
- 在 Windows DC 中绘图时,只使用高级、大面积的 GDI(API)函数!
这就是缓冲的作用。通过创建内存 DC 并将其选入一个位图,您可以直接在 RAM 中进行绘图。当您必须使用速度较慢、较低级别的 GDI 函数(如 SetPixel
)进行绘图时,您应该始终在内存 DC 中绘图,而不是在窗口 DC(显存)中。这将显著加快绘图速度。现在请记住,当您在内存 DC 中绘图时,您无法看到结果。您需要以某种方式将图像从 RAM(内存 DC)移动到显存(窗口 DC)。这就引出了第二点。
在实际显存(WM_PAINT
期间的窗口 DC)中绘图时,应使用覆盖更大面积的更复杂的 GDI 函数。例如,PatBlt
通常用于填充 Windows DC 的背景。这些类型的 GDI 函数(覆盖大面积)比低级函数更具优化性。使用缓冲区(内存 DC)时,更常用的 GDI 函数之一是 BitBlt
。BitBlt
经过高度优化,可以在 DC 之间来回移动大量数据(像素)。BitBlt
通常用于将图像已绘制的内存 DC 中的数据(像素)移动到窗口 DC(即显存)中。另一个非常 GDI 函数是 StretchBlt
,它可以将图像缩放到任何大小的窗口 DC。
通过使用这些非常快速且高度优化的 GDI(API)函数,您可以显著加快在窗口 DC 中绘图的速度。
注意:在打印机 DC 中绘图时,情况略有不同,因为许多打印机驱动程序不支持 BitBlt
。在这种情况下,需要使用 DIB(设备无关位图),并使用 StretchDiBits
等 GDI 函数。
将上述两个原理付诸实践是使用内存 DC 作为缓冲器的基础。使用内存缓冲区也有助于为窗口 DC 添加持久性。而不是每次处理 WM_PAINT
消息时都必须从头开始在窗口 DC 中重绘图像,可以使用 BitBlt
将单个图像从内存复制到窗口 DC。将此技术视为“绘制一次,复制多次”。
除了这个,还有其他可以提高速度的绘图技术,但它们有点更高级。其中一种是使用 DIB。简单来说,DIB 是一个类似于位图的内存图像,但区别在于它是设备无关的,您可以实际选择数据存储的格式。例如,如果视频显示是 16 位颜色,您可以将数据复制到一个存储为 32 位颜色的 DIB 中。另一个重要的区别是,对于 DIB,您可以直接以字节形式访问每个像素。GDI 不是访问像素所必需的。这有点像以前可以直接访问显存的方式。在这种情况下,有几个额外的步骤。您创建一个与视频模式兼容的内存 DC 和内存位图(例如,256 色、16 位等)。然后,您将数据从内存位图(和 DC)移动到 DIB 部分(即分配的用于保存字节块的 RAM 块)。现在,您可以随意直接访问 DIB 部分的数据。您可以编写自己的自定义绘图函数,直接处理 RAM 数据。完成绘图后,您将 DIB 部分中的数据移回内存 DC 位图。现在,您可以将内存 DC Bitblt 到显存中。我认为在使用 DIB 时也可以跳过内存 DC 步骤,并在窗口 DC 和 DIB 部分之间来回移动数据,但我还没有尝试过,也无法验证它的工作原理。
只是为了补充我的图形学 101
我无意深入探讨 Windows 绘图的底层技术细节。当然,实际情况比我提到的要复杂得多。视频驱动程序和 GDI 介于您的代码和直接显存之间,您无法直接访问它。
我的观点是,当您在窗口 DC 中绘图时,在实际应用中,您是在间接在显存中绘图(您绘制的内容存储在那里)。在窗口 DC 中绘制的内容的问题在于它没有持久性。存储窗口 DC 像素的内存(显存或其他)是所有窗口共享的。DC 本身可能被共享(它存储当前对象,如画笔、画刷)或不共享,但存储像素的区域是所有窗口共享的。任何其他窗口都可以覆盖您窗口在显存中的像素数据。由于其缺乏持久性,因为它与任何可能在其上绘图的窗口共享像素空间,因此窗口 DC 的像素数据应始终被视为临时的。
内存 DC(以及与之关联的位图,像素实际存储在那里)可以与其他窗口隔离访问。您可以创建自己的内存 DC 和内存位图进行绘图,而其他人无法打扰它。这产生了持久性。
内存 DC(选入其内存位图)有两个优点。一是绘图速度。在内存 DC 中绘图总是比在窗口 DC 中绘图快(无论出于何种原因)。二是内存 DC(及其位图)具有持久性,而窗口 DC 没有。
为了证明持久性的概念,编写一个只处理一次 WM_PAINT
消息(第一次调用时)的程序,看看当您用另一个窗口覆盖它时会发生什么。
持久性课程!
注意:下面的代码使用 PowerBASIC DDT 语法,简化了对话框的创建。
下面是一个程序,演示了窗口 DC 未持续重绘时发生的情况。它演示了在窗口 DC 中绘图时缺乏持久性。如果阻止窗口处理 WM_PAINT
(或 WM_ERASEBKGND
)消息,则在窗口 DC 中绘制的像素不会被记住。
#COMPILE EXE
#REGISTER NONE
#DIM ALL ' This is helpful to prevent errors in coding
#INCLUDE "win32api.inc" ' Must come first before other include files !
DECLARE SUB LIB_InitColors()
DECLARE SUB LIB_DeleteBrushes()
DECLARE SUB ShowDialog_Form1(BYVAL hParent&)
DECLARE CALLBACK FUNCTION Form1_DLGPROC
DECLARE SUB ShowDialog_Form2(BYVAL hParent&)
DECLARE CALLBACK FUNCTION Form2_DLGPROC
DECLARE CALLBACK FUNCTION CBF_FORM2_BUTTON1()
%FORM2_BUTTON1 = 100
GLOBAL App_Brush&()
GLOBAL App_Color&()
GLOBAL App_Font&()
GLOBAL hForm1& ' Dialog handle
GLOBAL hForm2& ' Dialog handle
GLOBAL PaintFlag&
' *************************************************************
' Application Entrance
' *************************************************************
FUNCTION PBMAIN
LOCAL Count&
LIB_InitColors
PaintFlag&=1
ShowDialog_Form1 0
ShowDialog_Form2 hForm1&
DO
DIALOG DOEVENTS TO Count&
LOOP UNTIL Count&=0
LIB_DeleteBrushes
END FUNCTION
SUB ShowDialog_Form1(BYVAL hParent&)
LOCAL Style&, ExStyle&
Style& = %WS_POPUP OR %DS_MODALFRAME OR %WS_CAPTION OR %WS_MINIMIZEBOX
OR %WS_SYSMENU OR %DS_CENTER
ExStyle& = 0
DIALOG NEW hParent&, "WM_PAINT limited window", 0, 0, 267, 177,
Style&, ExStyle& TO hForm1&
DIALOG SHOW MODELESS hForm1& , CALL Form1_DLGPROC
END SUB
CALLBACK FUNCTION Form1_DLGPROC
SELECT CASE CBMSG
CASE %WM_PAINT
' Windows calls WM_ERASEBKGND to fill background
' so I process that message
CASE %WM_ERASEBKGND
IF PaintFlag&=0 THEN
FUNCTION=1
EXIT FUNCTION
END IF
' -----------------------------------------------
CASE %WM_CTLCOLORDLG
IF CBLPARAM=CBHNDL THEN
' Dialogs colors
SetTextColor CBWPARAM, App_Color&(0)
SetBkColor CBWPARAM, App_Color&( 17)
FUNCTION=App_Brush&( 17)
END IF
CASE ELSE
END SELECT
END FUNCTION
SUB ShowDialog_Form2(BYVAL hParent&)
LOCAL Style&, ExStyle&
Style& = %WS_POPUP OR %DS_MODALFRAME OR %WS_CAPTION OR %WS_MINIMIZEBOX
OR %WS_SYSMENU OR %DS_CENTER
ExStyle& = 0
DIALOG NEW hParent&, "Click Button to Toggle painting of other window",
0, 0, 245, 59, Style&, ExStyle& TO hForm2&
CONTROL ADD "Button", hForm2&, %FORM2_BUTTON1, "Toggle WM_PAINT for
other Window", 37, 17, 176, 15, _
%WS_CHILD OR %WS_VISIBLE OR %BS_PUSHBUTTON OR %WS_TABSTOP CALL
CBF_FORM2_BUTTON1
DIALOG SHOW MODELESS hForm2& , CALL Form2_DLGPROC
END SUB
CALLBACK FUNCTION Form2_DLGPROC
SELECT CASE CBMSG
' -----------------------------------------------
CASE %WM_CTLCOLORDLG
IF CBLPARAM=CBHNDL THEN
' Dialogs colors
SetTextColor CBWPARAM, App_Color&(0)
SetBkColor CBWPARAM, App_Color&( 10)
FUNCTION=App_Brush&( 10)
END IF
CASE ELSE
END SELECT
END FUNCTION
' *******************************************************************
' * Library Code *
' ********************************************************************
SUB LIB_InitColors()
DATA 0, 8388608, 32768, 8421376, 196, 8388736,
16512, 12895428
DATA 8421504, 16711680, 65280, 16776960, 255, 16711935,
65535, 16777215
DATA 10790052, 16752768, 10551200, 16777120, 10526975, 16752895,
10551295, 13948116
DATA 11842740, 16768188, 14483420, 16777180, 14474495, 16768255,
14483455, 15000804
LOCAL T&, RGBVal&
REDIM App_Brush&(0 TO 31)
REDIM App_Color&(0 TO 31)
FOR T&=0 TO 31
RGBVal&=VAL(READ$(T&+1))
App_Brush&(T&)=CreateSolidBrush(RGBVal&)
App_Color&(T&)=RGBVal&
NEXT T&
END SUB
' -------------------------------------------------------------
SUB LIB_DeleteBrushes()
LOCAL T&
FOR T&=0 TO 31
DeleteObject App_Brush&(T&)
NEXT T&
END SUB
' -------------------------------------------------------------
CALLBACK FUNCTION CBF_FORM2_BUTTON1
IF CBCTLMSG=%BN_CLICKED THEN
IF PaintFlag&=0 THEN
PaintFlag&=1
ELSE
PaintFlag&=0
END IF
END IF
END FUNCTION