位图基础 - GDI 教程






4.70/5 (62投票s)
2000 年 3 月 4 日

714885

38126
一组关于处理核心位图结构的基础教程
Windows GDI 教程 1 - 绘制位图
位图和调色板可能都是 GDI 子系统中对新手程序员来说最有用的部分,也可能是最令人困惑的部分。在本教程及后续的 GDI 教程中,我将解释如何将位图绘制到窗口上,如何实现位图透明,以及如何在窗口中绘制动画而没有任何闪烁或跳跃。这是第一个教程,我将从解释设备上下文的基本概念以及如何正确使用它们开始。
GDI & DC
GDI 代表“图形设备接口”(Graphics Device Interface),DC 代表“设备上下文”(Device Context)。Windows 的设计者决定需要一种单一的方式来绘制到所有“事物”上,因此开发了 GDI 作为一套通用的例程,可以用于绘制到屏幕、打印机、绘图仪或内存中的位图图像。GDI 库围绕一个名为设备上下文(Device Context)的对象构建。设备上下文是某个设备上的绘图表面的句柄——通常可以获取显示设备(整个屏幕)、打印机和绘图仪的设备上下文。最常使用的是窗口 DC(一个仅仅代表单个窗口区域的显示 DC)以及代表内存中位图作为设备的内存 DC。
这些对象(显示、打印机、位图等)的共同点是它们都有某种“绘图表面”的概念,输出将显示在那里。
与设备上下文相关联的是一系列可用于操作关联绘图表面的工具:画笔、画刷、字体等。对于绘图仪等物理设备,将有一个 HPEN
到物理画笔的一对一映射。对于显示或内存 DC,提供了许多预设画笔,并且可以根据需要动态创建更多画笔。
位图
位图是绘图表面的内存表示。通过将位图“选择”到内存 DC 中,该 DC 就代表该位图作为绘图表面,并且所有常规的 GDI 操作都可以对位图执行。GDI 还提供了一些函数,可以将一个 DC 的绘图表面的区域复制到另一个 DC,因此位图是存储将在以后复制到显示器(或其他设备)的图像的有用方式。可以被选择到 DC 中的位图称为“设备相关位图”(Device Dependent Bitmap),并由 HBITMAP
句柄向程序员表示。
还有另一种类型的位图称为“设备无关位图”(Device Independent Bitmap)。这种位图在 Windows 头文件中定义为一组由程序员填充的结构。 being "device independent" means there is no HBITMAP
that can be selected into a "Device context" so GDI operations cannot be performed on this type of bitmap. 有几个“DIB”特定的函数可以给定一个 DIB 来创建 DDB(设备相关位图),或者将 DIB 的区域复制到 DC。
教程
在本教程(GDI01)中,我将演示如何将位图资源加载为设备相关位图,以及如何在窗口上显示位图。位图在应用程序的WM_CREATE
处理程序中加载,并在应用程序的 WM_PAINT
处理程序中显示。在 GDI 教程 2 中,我将演示如何使用设备相关位图实现位图透明。
本教程包含一个单一的窗口,我在 main.cpp 中创建。位图句柄存储在一个全局变量中,它在 OnCreate()
函数中初始化,在 OnPaint()
中使用,并在 OnDestroy()
中销毁。教程附带的示例位图是一个 256 色图像,在 256 色显示器上会显得有些平淡。在教程 3 中,我打算讨论调色板,届时将解决 256 色显示器上缺乏适当颜色显示的问题。
Windows GDI 教程 2 - 透明度
简单的位图图形功能非常强大。然而,很快就会遇到需要重叠两个非矩形图像的情况。Windows 位图“不幸地”是矩形的,所以需要做的是将位图的某些区域标记为“不属于图像”。换句话说:透明。GDI 没有内置的透明度支持——你必须自己实现位图的透明度。GDI 的特定版本曾支持位图中的透明区域——例如 NT4 有一个特定的函数,而 Windows 3.11 的 VFW 工具包包含一个扩展的 devmode 选项,可以设置为 DC 以指定 SetBkColor 设置的颜色将是透明的。然而,这些方法与其他平台(尤其是 Windows 95)不兼容,应该避免使用。
一种简单的方法
实现具有“透明”区域的位图的最简单方法是使用两个位图。一个位图指定图像——所有“透明”区域都设置为黑色。另一个位图是单色的/黑白的。这个位图的边缘有白色像素。黑色像素形成图像的轮廓。GDI 在组合 DC 表面的内容时支持布尔运算,我们在这里利用这一点。要将“透明”位图对绘制到 DC 上,将执行以下过程:
- 使用
SRCAND
作为光栅代码,将单色位图绘制到目标上。SRCAND
代码指示 GDI 将每个目标像素设置为目标像素和源像素的二进制 AND。在这种情况下,黑色充当零,白色充当所有 1:源为黑色的目标像素变为黑色。源为白色的目标像素保持不变。效果是目标图像被挖出了一个洞。 - 使用
SRCPAINT
将彩色图像位图绘制到目标上。SRCPAINT
光栅代码指示 GDI 将每个目标像素设置为先前目标值和源像素的二进制 OR。现在,由于上一步,无论源的像素是否为黑色,目标都已被归零。零 OR 任何东西都是那个东西。因此,这一步无缝地组合了两个图像。
另一种方法
上述具有成对位图(彩色图像位图和单色蒙版)的方法,在 GDI 操作方面是最简单的。然而,有些人不喜欢提供第二个单色蒙版图像:他们通过指定一种颜色作为透明颜色来指定位图中的透明区域。这种颜色填充了图像周围的区域,并且必须小心不要在图像本身中使用“魔法”颜色。另外,在使用这种位图与低色彩显示器上的 GDI 配合时,必须格外小心:GDI 总是创建“兼容”的 DDB(并且你程序员总是想使用“兼容”位图),其格式与显示模式相同。这可能导致颜色“分辨率”的损失,并且一系列颜色可能会被映射到魔法透明色。因此,最好确保透明色是二十种系统颜色之一,这些颜色保证始终存在。
单色位图
处理单色位图最初可能会很棘手,因为缺乏明确的文档说明它们如何与彩色位图交互。在示例代码中,我直接从彩色位图绘制到单色位图,稍后在反向方向上也是如此。要弄清楚在这种情况下会发生什么,你必须知道 GDI 如何将单色位图像素映射到彩色位图像素。位图的背景“颜色”是白色,存储为二进制 0。当通过光栅操作(通常在调用 BitBlt 时)与彩色位图组合时,单色位图中的背景像素首先被映射到彩色位图 DC 的背景色。通常设置为白色(RGB(255,255,255)),但可以使用 SetBKColor()
API 轻松更改。单色位图的前景像素(二进制 1)被映射到目标 DC 的文本颜色——默认为黑色(RGB(0,0,0)),但同样可以使用 SetTextColor()
API 来更改。
将字节从彩色位图传输到单色位图时,映射更简单。所有颜色与背景颜色相同的像素都映射到单色位图的背景颜色(0)。所有其他像素都被视为前景。
光栅操作
光栅操作(SRCPAINT
、SRCCOPY
)等是在任何映射发生后才执行的。它们是按字节对图像字节执行的。这是最高效的操作方式,但它意味着在 256 色显示器上执行的逻辑光栅操作往往会产生意外结果,因为调色板在此过程中被完全忽略。默认的二十种系统颜色将表现出预期的方式,因为系统调色板已特别安排,以使映射生效。系统调色板不只是简单地使用前二十种颜色,而是使用前十种和后十种颜色,因此当对黑色(颜色索引 0)执行 NOT 时,NOT 操作的结果(颜色索引 255)就是预期的白色。
教程
代码演示了如何从彩色图像创建单色位图。代码使用一种简单的方法来确定用作透明色的颜色——它检查左上角像素的颜色。编译并运行后,文件应显示一个带有烦人的棋盘格图案的窗口。透明图像绘制在这个棋盘图案之上。下载文件列在本文章的顶部。main.cpp 中相关的函数都有详细注释。查看 WM_CREATE
处理程序,其中加载了主位图并生成了单色版本。WM_PAINT
处理程序演示了如何正确绘制两个位图。WM_DESTROY
清理两个位图。还可以查看框架的 RegisterClass()
函数,了解棋盘格背景是如何设置的。
后续内容
在下一篇 GDI 教程中,我将讨论
- 手动加载位图资源。
- 从位图资源创建位图和调色板。
- 正确使用调色板以确保在 256 色显示硬件上的颜色保真度。
Windows GDI 教程 3 - 使用调色板和访问位图资源
尝试弄清楚如何实现调色板支持可能是一个相当混乱的过程。现有的文档从未完全清楚应该采取哪种方法以及在不起作用时该怎么做。所以……
直接加载资源
位图资源和磁盘上的位图文件都以 Windows DIB 格式存储。作为资源,位图由一个描述位图的BITMAPINFO
结构组成,后面跟着实际的图像数据,作为字节数组。在磁盘上的 .bmp 文件中,文件以 BITMAPFILEHEADER
结构开头,后面跟着 BITMAPINFO
结构。图像数据的起始位置由 BITMAPFILEHEADER
结构中的一个字段指示,并不一定紧跟在 BITMAPINFO
结构之后。这种差异在处理位图资源和位图文件时会引入一些令人恼火的不兼容性。LoadBitmap()
函数虽然易于使用,但对于需要调色板支持的应用程序来说太“傻瓜”了,因为它使用只有 20 种颜色的系统默认调色板创建所有位图。虽然这只在 256 色显示设置中成问题,但这是一个非常糟糕的问题——所有加载的位图都只显示 20 种颜色。
解决方案是使用资源函数直接加载位图,使用资源函数在 exe 文件中搜索位图资源,并获取指向资源数据的指针。由于我们知道数据是以 DIB 格式存储的,我们可以使用 CreateDIBitmap()
API 从 DIB 数据创建 DDB。
调色板和位图
对于 Windows 来说,位图只是一个字节数组。在 16 位及更高模式下,位图数据的颜色信息直接编码在数据中。然而,在 256 色模式下,位图中不存储颜色信息。位图中的每个字节只是一个颜色索引到调色板。因此,GDI 对位图执行的任何操作都将使用当前选择的逻辑调色板。
请注意,“逻辑调色板”一词指的是 GDI 调色板对象——由 HPALETTE
句柄引用。物理调色板指的是实际显示设备调色板的状态。
现在,将位图绘制到显示器上的最快方法是简单的 memcpy 操作。GDI 会尽可能多地这样做。然而,为了使结果看起来令人愉悦,位图的字节必须与物理调色板中的正确条目匹配。为了确保这一点,GDI 在首次实现(realize)调色板时,会创建一个逻辑调色板条目到当时系统调色板的映射。GDI 期望下次实现调色板时,能够采取相同的映射。
那么,位图中的字节是从这个缓存表而不是逻辑调色板中绘制的。
总之。整个主题非常复杂,我只能建议如果你想真正理解这个主题,请仔细阅读你找到的所有关于调色板的可用文档。
以下说明可能会减轻一些潜在的困惑
- 实现(Realization)是一次性的。一旦对
HDC
调用了RealizePalette()
,该HPALETTE
就无需再次实现,直到调用UnRealizePalette()
,或者WM_PALETTECHANGED
或WM_QUERYNEWPALETTE
表明调色板管理器本身已经取消了所有调色板的实现并重新开始。你可以随意选择和取消选择HPALETTE
,而无需调用RealizePalette()
。 RealizePalette()
的最后一个参数始终可以设置为FALSE
。TRUE
仅在你实现窗口 DC 并且明确不希望调色板被映射到物理调色板时使用。- 始终确保将位图创建时使用的调色板选择到你将位图选择进去的任何 DC 中(顺序不重要)。
- 内存 DC 之间的 BitBlt 要求目标 DC 具有与源 DC 相同的调色板。
- 从内存 DC 到屏幕 DC 的 BitBlt 要求源调色板已实现。
Windows GDI 教程 4 - DIB
欢迎来到 GDI 教程 4 - 在本教程中,我将专注于设备无关位图。
DIB 和位图资源
正如现在应该已经很清楚的,位图资源以设备无关位图的形式存储。资源数据包含BITMAPINFO
结构,后面跟着一个字节数组。将指向这两个结构的指针传递给可以使用 DIB 的所有 Windows API 函数,可以使加载的位图资源直接使用。应注意的一个限制:由于资源是从其加载的 exe 或 dll 文件中分页出来的,因此应小心避免写入内存。在 Win16 中,写入资源的所有更改如果资源被解锁然后重新锁定,可能会丢失。在 Win32 中,写入资源内存会导致内存异常,操作系统会处理该异常以创建资源的副本。
DIB 和位图文件
位图文件在加载到内存后也可以直接用作 DIB。文件与资源之间的唯一区别是文件以BITMAPFILEHEADER
结构开头。该结构紧跟其后的是 BITMAPINFO
结构,其中包含有关 DIB 的信息以及颜色表(如果存在)。BITMAPFILEHEADER
结构不幸地还包含一个指向 DIB 字节数组的文件偏移量,因此在位图文件中,字节数组可能不会紧跟在 BITMAPINFO
结构之后。如果位图数据不直接跟在 BITMAPINFO
结构之后,某些资源编译器无法正确处理位图文件。它们会将填充的信息写入资源——在这种情况下,位图加载代码无法知道标题和位图之间存在间隙,图像将显示为损坏。
其他图像格式和 DDB
处理图像最有效的方法是使用设备相关位图。GDI 以显示设备的格式存储 DDB 位图——因此 DDB 是处理图像最有效的方式。然而,GDI 不允许程序员直接访问 DDB 的位。不过,GDI 提供了一些函数,允许程序员将数据从 DIB 传输到 DDB,以及反向传输。这些传输操作很慢,因为 GDI 必须将源 DDB 或 DIB 的每个像素转换为其在目标 DIB 或 DDB 上的最近表示。因此,在加载或保存位图以外的图像格式的 DDB 时,程序员通常会发现自己正在使用 DIB 的数据。
GDI 提供以下函数来将 DIB 的位传输到 DDB,DDB 的位传输到 DIB,以及 DIB 的位传输到 DC
CreateDIBitmap()
- 此函数创建一个兼容的设备相关位图,并使用传入的 DIB 进行初始化。GetDIBits()
- 此函数将 DDB 的数据转换为传入的 DIB。SetDIBits()
- 类似于CreateDIBitmap()
,此函数使用传入的 DIB 数据初始化 DDB。SetDIBitsToDevice()
- 此函数直接将 DIB(当然是转换了每个像素)复制到显示设备上下文。StretchDIBits()
- 此函数类似于StretchBlt()
,它将源拉伸到 DC 的表面——源数据是 DIB。
DIB 和调色板及转换
在SetDIBits()
、SetDIBitsToDevice()
或 StretchDIBits()
调用中将数据从 DIB 转换到 DDB 时,GDI 需要做很多工作。如果目标显示设备在 256 色(或其他调色板)模式下运行,则工作量更大。GDI 用于转换像素的逻辑如下:首先,GDI 解析它正在转换的源(DIB)像素的 RGB 值。如果 DIB 本身有颜色表,则在颜色表中查找像素索引,并使用检索到的 RGB 颜色进行 GDI 操作。现在,有了 RGB 值,GDI 会查找 RGB 值并将其与设备上下文的调色板中找到的最接近的颜色进行匹配。(所有上述调用都接受一个设备上下文——在某些情况下,DC 仅仅是 hpalette 的载体)。然后,逻辑调色板中的颜色与物理调色板中的颜色进行匹配,任何光栅操作(在 SetDIBitsToDevice()
或 StretchDIBits()
的情况下)现在都使用物理索引应用,并将结果存储(在显示器或目标 DDB 上)。
在非调色板设备上,情况要简单得多。找到 DIB 像素的 RGB 值后,它会与给定光栅操作的目标 RGB 结合。
DIB 透明度
使用 DDB 实现透明度的一种常用方法是使用一个彩色位图和一个包含蒙版的关联单色位图。蒙版位图使用SRCAND
光栅操作与目标 DC 组合。所有白色像素保持目标不变,所有黑色像素将目标置零——有效地在目标中切出一个黑色孔。然后使用 SRCPAINT
光栅操作将彩色位图与目标组合。此操作将彩色图像的每个彩色像素与目标中被黑化的像素进行 OR 运算。源图像本身在目标已被黑化的位置是黑色的,使非透明像素保持不变——透明像素现在包含图像。对于 DIB,使用类似的方法,但 DIB 方法不需要两个完整的 DIB *如果* DIB 有颜色表。通过两次绘制相同的 DIB 数据,一次使用 SRCAND
并将颜色表初始化为所有透明颜色索引设置为白色,并将“数据”索引设置为黑色,一次使用 SRCPAINT
并将颜色表设置为包含正确颜色的数据索引,以及将透明索引设置为黑色,可以实现相同的效果。
注意:在 256 色显示器上,如果目标 DC 中选择的逻辑调色板不包含黑色和白色的条目,则 DIB 方法将失败。这是因为(如上所述)DIB 像素首先与逻辑调色板中最接近的条目匹配,然后找到的逻辑调色板条目被映射到物理调色板(该调色板始终包含包括黑色和白色在内的 20 种系统颜色),然后才执行光栅操作。
教程
到目前为止,你应该能识别出这个简单的教程。你将再次看到飞机在云背景上的显示。这一次,飞机和云是使用 DIB 显示的。demo_OnPaint()
函数照例包含所有有趣的内容。除了 bmpapi 文件外,dib.cpp 和 dib.h 中还有一个原始的 DIB 容器类。你需要的文件都在本文的顶部。