15.2 显示和打印
2015-11-29 09:09
357 查看
摘录于《Windows程序(第5版,珍藏版).CHarles.Petzold 著》P594
位图是显示给用户看的。在本节,我们先来看一下 Windows 支持的两个函数,它们可以把 DIB 显示到视频显示器或打印页上。如果想追求更好的性能,最终你可能更愿意采取一种不太直接的方法来显示位图,我将在本章的后面进行讨论。这两个函数只是一个开端。
这两个函数分别是 SetDIBitsToDevice 和 StretchDIBits。每个函数都使用存储在内存中的 DIB,并且可以显示整个 DIB,或者 DIB 的部分矩形区域。使用 SetDIBitToDevice 时,所显示图像的像素大小和 DIB 的像素数一样。例如,一个 640 * 480 的 DIB 会占据整个标准 VGA 屏幕,但是在 300dpi 的激光打印机上,这个 DIB 只有 2.1 * 1.6 英寸。StretchDIBits 函数能通过拉伸或缩小 DIB 的行数和列数,把它按照特定大小显示到输出设备。
DIB 文件可以加载到内存中。如果整个文件(除了文件头)都存储在一个连续的内存块中,则指向此内存块开头的指针就被称为指向紧凑 DIB 格式,如下图所示:
通过剪贴板传输 DIB 时你就需要这种格式。从 DIB 创建一个画刷也同样需要这种格式。紧凑 DIB 格式使 DIB 能方便地存储在内存中,因为整个 DIB 被单个指针引用(例如,pBackedDib 指针),而这个指针可以像 BYTE 类型的指针一样定义。根据本章早前所展示的结构定义,你可以获取
DIB 存储的所有信息,包括颜色表和单个像素位。
然而,要得到这么多信息,需要写若干行代码。例如,想获取 DIB 的像素宽度,不能简单地使用下面的语句:
现在让我们做一个有趣的练习:给定一个指向紧凑 DIB 的指针,请找出坐标点(5, 27) 的像素值。即使假设 DIB 不是 OS/2 兼容格式,也需要知道 DIB 的宽度、高度和每像素的位数。需要计算每个像素行的字节长度。还需要确定颜色表的元素个数,以及颜色表是否包含三个 32 位颜色遮罩。此外,还要检查 DIB 是否已经被压缩,如果是,就不能直接访问像素。
如果想直接访问 DIB 的所有像素(就像进行图像处理时做的一样),上述做法可能会增加不少处理时间。因此,尽管维护一个指向紧凑 DIB 的指针很方便,但是这不代表能写出有效率的代码。绝佳的解决方案就是为 DIB 定义一个 C++ 类,它包含足够多的成员数据,使你能快速随机地访问 DIB 像素。然而,在本书开始,我才鞥允诺过你不需要知道任何 C++ 知识,所以我会在第 16 章给出 C
版本的解决方案。
对于 SetDIBitsToDevice 和 StretchDiBits 函数,你需要知道的信息包括一个指向 DIB 的 BITMAPINFO 结构的指针。你可能还记得,BITMAPINFO 结构由 BITMAPINFOHEADER 结构和颜色表组成。所以,这其实就是一个经过适当的强制转换的指向紧凑 DIB 的简单指针。
这两个函数还要求有像素位的指针。这可以从信息头里的信息得到,只是代码不漂亮而已。注意,如果你能获取 BITMAPFILEHEADER 结构的 bfOffBits 字段,计算指针就简单一些。bfOffBits 字段指示了从 DIB 文件开头到像素位的位移量。你可以简单地把这段位移加到 BITMAPINFO 指针上,再减去 BITMAPFILEHEADER 结构的大小。但是,如果指针指向的是剪贴板的紧凑
DIB,这种方法是行不通的,因为你没有 BITMAPFILEHEADER 结构。
下图给出了需要用到的两个指针:
SetDIBitsToDevice 和 StretchDIBits 函数需要两个指向 DIB 的指针,因为它们指向的两个部分不一定在一段连续的内存块中。可能有以下两个内存块:
事实上,像这样把一个 DIB 分成两个内存块存放非常有用。目前我们讨论的仅仅是紧凑 DIB,所以才把整个 DIB 存放在单个内存块中。
除了这两指针,SetDIBitsToDevice 和 StretchDIBits 函数也需要知道 DIB 有多少像素宽、多少像素高。如果只显示 DIB 的一部分,就不需要知道它们的具体值,但如果需要在 DIB 像素位数组中定义一个矩形,则他们就确定了矩形的宽和高的上限。
SetDIBitsToDevice 函数:
和 GDI 显示函数一样,SetDIBitsToDevice 的第一个参数是指向设备环境的句柄,它指明了你想要在什么设备上显示 DIB。接下来的两个参数,xDst 和 yDst,是输出设备的逻辑坐标点,指明了 DIB 图像的左上角将在什么坐标上显示。(这里左上角的“上”指的是图像的视觉上方,而不是 DIB 的第一个像素行。)注意,这两个参数是逻辑坐标,所以它们受实际映射模式的限制,或者——在 Windows NT 下——就是你可能设置的任何变换。在默认 MM_TEXT 映射模式下,你应该把这两个参数都设为
0,以便从显示表面的左上端起显示 DIB 图像。
你可以显示整个 DIB 图像,也可以只显示一部分。这就是接下来四个参数的作用。有时这几个参数的使用会造成这种情况:DIB 像素数据的上下倒置带来现实中的扭曲。我将很快给予讨论。暂时记住,要显示整个 DIB,则应把 xSrc 和 ySrc 设成 0,cxSrc 和 cySrc 分别等于 DIB 的像素宽度和高度。注意,对于自上而下的DIB,BITMAPINFOHEADER 结构的 biHeight 字段是负数,所以 cySrc 应该设置成 biHeight 字段的绝对值。
这个函数的 MSDN 文档(/Platform SDK/Graphics and Multimedia Services/GDI/Bitmaps/Bitmap Reference/Bitmap Functions/SetDIBitsToDevice)(译注:/MSDN Library/Win32 and Com Development/Graphics and Multimedia/Windows
GDI/Bitmaps Reference/Bitmap Functions)介绍说 xSrc, ySrc, cxSrc, cySrc 参数是逻辑单位。这并不正确。它们是像素坐标点和像素尺寸。对 DIB 里的像素讨论逻辑坐标点和单位是没有意义的。另外,无论映射模式是什么,在输出设备上显示的 DIB 总是 cxSrc 个像素宽,cySrc 个像素高。
我将暂时跳过对接下来两个参数 yScan 和 cyScan 的详细讨论。在从磁盘文件或从一个拨号网络连接上读取像素位的时候,这两个参数通过按顺序每次显示 DIB 的一部分,来减少显示 DIB 所需的内存。通常,yScan 设成 0,cyScan 设成 DIB 的高度。
pBits 参数是一个指向 DIB 像素位的指针。pInfo 参数是一个指向 DIB 的 BITMAPINFO 结构的指针。尽管 BITMAPINFO 结构的地址和 BITMAPINFOHEADER 结构的地址一样,但 SetDIBitsToDevice 函数被定义成使用 BITMAPINFO 结构,这是由于:对于 1 位、4 位和 8 位 DIB 来说,位图信息头后面必须跟有一个颜色表。虽然
pInfo 参数定义成指向 BITMAPINFO 结构的指针,它也可以是指向 BITMAPCOREINFO,BITMAPV4HEADER 或者 BITMAPV5HEADER 结构的指针。
最后一个参数可以是 DIB_RGB_COLORS,或者是 DIB_PAL_COLORS,它们在 WINGDI.H 中分别定义为 0 和 1。你暂时会使用 DIB_RGB_COLORS,即表明 DIB 带有颜色表。DIB_PAL_COLORS 标识意味着 DIB 里的颜色表将被由设备环境选中并实现的 16 位索引的逻辑调色板取代。第 16 章将介绍这个选项。现在,可以只用 DIB_RGB_COLORS,或者如果你比较懒,就简单地用
0。
SetDIBitsToDevice 函数返回的是它显示的扫描行的个数。
综上所述,为了调用 SetDIBitsToDevice 以显示整个 DIB 图像,需要以下信息。
hdc 指向目标设备的设备环境句柄。
xDst 和 yDst 图像左上角的目标坐标点。
cxDib 和 cyDib DIB 的像素宽度和高度,cyDib 是 BITMAPINFOHEADER 结构的 biHeight 字段的绝对值。
pInfo 和 pBits 指向位图信息部分和像素位的指针。
然后可以如下调用 SetDIBitsToDevice:
下面所示的 SHOWDIB1 程序通过使用 SetDIBitsToDevice 函数显示一个 DIB。
DIBFILE.C 文件含有显示打开文件和保存文件对话框的例程,也包含把整个 DIB 文件(带有完整的 BITMAPFILEHEADER 结构)加载到单个内存块的例程。此程序也能把这样的内存块写到文件中。
SHOWDIB1.C 在处理文件打开命令时加载 DIB 文件,然后此程序计算 BITMAPINFOHEADER 结构的位移和内存块中的像素位。此程序还能获取 DIB 的像素宽度和高度。所有这些信息存储在一些静态变量里。处理 WM_PAINT 消息时,程序通过调用 SetDIBitsToDevice 显示 DIB。
当然,SHOWDIB1 缺少了一些功能。例如,对于客户区而言,DIB 如果太大,图像就没法通过滚动条来滚屏。这些不足将在第 16 章结束前弥补。
回顾一下过去,OS/2 Presentation Manager 中的 DIB 像素位是按由下而上的方向定义的,这种定义在某种程度上有些道理,因为在 PM 中的每个对象都以默认左下角为源点。例如,在 PM 窗口中,默认的(0, 0)原点是窗口的左下角。(如果觉得这比较古怪,说明你还是个普通人。如果不觉得有什么不妥,那你可能就是一个数学家。)位图绘制函数也以左下标点来指定目标位置。
因此,如果在 OS/2 中为位图指定了(0, 0)为目标坐标点,那么图像就会如图 15-3 所示显示在窗口的左下端。
在足够慢的机器上,你也许还能看到位图是从下而上绘制的。
图 15-3 在 OS/2 下显示的以(0, 0)为目标的位图
尽管 OS/2 坐标系可能看起来很奇怪,但它的优点是一致性。位图的(0, 0)原点总是位图文件第一行的第一个像素,并且此像素被映射到位图绘制函数给出的目标坐标中。
Windows 的问题是它不能保持内部的一致性。如果想显示整个 DIB 图像里的一个矩形子集,会使用参数 xSrc,ySrc,csSrc 和 cySrc。这些源坐标和大小是相对于 DIB 的第一行数据而言的,也就是图像的最底行。这跟 OS/2 很像。但是,与之不同的是, Windows 在目标坐标上显示图像的最顶行。因此,如果要显示整个 DIB 图像,在(xDst, yDst)坐标点显示的像素是位于(0, cyDib-1)坐标点的 DIB 像素。这是 DIB 数据的最后一行,但是是图像的顶行。如果只显示图像的一部分,在(xDst,
yDst)显示的像素是位于(xSrc, ySrc+cySrc-1)的 DIB 像素。
图 15-4 显示的图能帮你明白坐标的对应关系。下面给出的 DIB 按照你可能设想的方式在内存中存储——即上下倒置。度量坐标系的原点与 DIB 像素数据的第一位一致。SetDIBitsToDevice 的 xSrc 参数从 DIB 左边开始度量,cxSrc
是到 xSrc 右边的图像宽度。这很直接明了。ySrc 参数从 DIB 数据的第一行(即图像底部)开始度量,cySrc 是从 ySrc 到最后一行数据(图像顶部0的图像高度。
图 15-4 常规(自下而上)DIB 的 DIB 坐标说明
如果目标设备环境使用了默认的 MM_TEXT 映射模式的像素坐标,则源矩形和目标矩形的顶点坐标关系见下表:
(xSrc, ySrc) 不映射到(xDst, yDst),这使得坐标关系变得混乱。在任何其他映射模式下,坐标点(xSrc, ySrc + cySrc - 1)将仍然映射到逻辑坐标点(xDst, yDst),并且图像看起来和在 MM_TEXT 模式下显示一样。
到目前为止,我们讨论了 BITMAPINFOHEADER 结构的 biHeight 字段取正值的正常情况。如果 biHeight 字段为负值,DIB 数据会按照合理的自上而下的方式排列。你可能会认为着解决了所有问题。如果是这样,只能说你太过天真了。
很明显,某些人会认为如果翻转一个自上而下排列的 DIB 的所有行,再把 biHeight 字段设成正值,结果应该跟正常的自下而上的 DIB 一样。设计 DIB 的矩形区域的已有代码不需要作任何改变。我认为这个想法是合理的,但是它忽视了一个现实:程序仍然需要被修改,以便在处理自上而下的 DIB 时避免使用负的高度值。
此外,这种做法的结果带有奇怪的含义。它意味着自上而下 DIB 的源坐标有一个位于 DIB 最后一个数据行的原点,也就是图像的最底行。这和我们遇到过的任何情况都不同。位于(0, 0)原点的 DIB 像素不再是 pBits 指针引用的第一个像素,也不是 DIB
文件的最后一个像素,而是处于中间的某个位置。
图 15-5 展示了怎样在自上而下的 DIB 中指定一块矩形区域。同样,图示的 DIB 代表了它在文件或内存中的实际存储方式。
图 15-5 自上而下 DIB 的 DIB 坐标说明
不管怎样,这种策略的实际优势在于 SetDIBitsToDevice 函数的参数与 DIB 数据取向无关。如果你有两个 DIB(一个自下而上,另一个自上而下)显示同一幅图像(即两个 DIB 文件的行排列顺序相反),你可以用相同的 SetDIBitsToDevice 函数参数显示图像的同一部分。
下面所示的 APOLLO11 程序演示了这一点。
这个程序加载了两个 DIB,分别是 APOLLO11.BMP(自下而上的版本)和 APOLLOTD.BMP(自上而下的版本)。两个 DIB 都是 220 个像素宽,240 个像素高。注意,程序在根据头信息结构确定 DIB 的宽度和高度时,使用了abs 函数来获得 biHeight 字段的绝对值。全部或部分显示 DIb 时,无论显示的是那幅位图,xSrc,ySrc,cxSrc 和 cySrc 坐标都相同。程序结果见图 15-7。
图 15-7 APOLLO11 的显示结果
注意,“第一行扫描行”和“扫描行个数”参数没有改变。我会很快讲解这两个参数。pBits 参数也保持不变。不要试图改变 pBits,让它仅仅指向你想要显示的 DIB 区域。
我在这个问题上花费了大量时间来讨论,并不是因为我想打击 Windows 开发者尽力整合有疑问的 APi 定义的努力,而是想让你在问题看起来费解时不那么紧张。这个问题是令人费解的,因为它本身就很乱。
我也想让你注意 Windows 文档里的某些语句,例如 SetDIBitsToDevice 文档中有这样一句话:“自下而上 DIB 的原点是位图的左下角:自上而下的 DIB 原点是左上角。”这不仅仅是模糊不清,而是完全错误的。我们可以像这样更好地表述二者的区别:自下而上DIB
的原点是位图图像的左下角,即位图数据的第一行的第一个像素行;自上而下的DIB 的原点也是位图图像的左下角,但在这种情况下,左下角是位图数据最后一行的第一个像素。
如果你想写一个函数,使程序访问 DIB 的单个像素位,情况将变得更糟。具体操作应该和显示部分 DIB 图像时指定坐标的方法保持一致。我的解决方案(我将在第 16 章的一个 DIB 库中实现)是引用 DIB 像素和坐标时,假想(0, 0)原点是 DIB 图像被正确显示时,你所见的 DIB 的顶行的最左边的像素。
即便如此,有时你可能不想把整个文件加载到内存后再显示 DIB。即使有足够内存容纳 DIB,把 DIB 移到内存中也可能会迫使 Windows 虚拟内存系统把其他代码和数据移到磁盘。如果碰到 DIB 仅仅是被用来显示,之后又立即从内存中删除的情况,这样的操作会很令人痛苦。
另外一个问题是:假设 DIB 位于一个慢速的存储媒介例如软盘上,或者来自于调制解调器,或是来自于一个从扫描仪或视频抓帧器获取像素数据的转换例程。你想等到整个 DIB 被加载到内存后才显示它吗?还是你宁愿在从磁盘或电话线或扫描仪得到
DIB 后立即按正确的顺序显示它?
SetDIBitsToDevice 函数的 yScan 和 cyScans 参数就是用来解决这些问题的。要使用它们,你得多次调用 SetDIBitsToDevice,其中的大多数调用使用同样的参数。然而,在每次调用中,pBits 参数需指向整个位图像素矩阵的不同部分。yScan 参数指出了 pBits 指向哪个像素数据行,cyScans 参数是 pBits 引用的数据行的个数。这个方法在很大程度上减少了内存需求。你只需要给
DIB 的信息部分(BITMAPINFOHEADER 结构和颜色表)以及至少一个像素数据行分配足够的内存即可。
举例来说,假设 DIB 有 23 行像素。你想按 5 行一个数据显示 DIB。这就可能需要分配一块存储 DIB 的 BITMAPINFO 部分的内存,此内存被 pInfo 变量引用。然后从文件中读数据。检查了这个数据结构的字段后,你就可以计算每行有多少个字节。用这个结果乘以 5,根据得出的值分配另一个内存块(pBits)。这之后就可以读入前 5 行数据,像平时一样调用 SetDIBitsToDevice 函数,不过 yScan 设为 5。继续调用这个函数,把 yScan 设成 10,再设成
15.最终,把最后 3 行读入被 pBits 引用的内存块,然后调用 SetDIBitsToDevice 函数,把 yScan 设成 20,cyScans 设成 3。
现在有个坏消息。首先,通过这种方式使用 SetDIBitsToDevice 要求你的程序里数据获取和数据显示两者之间要高度耦合。这通常不太好,因为你必须在获取数据和显示数据之间切换。这样,整个过程会变慢。其次,SetDIBitsToDevice 是唯一有此功能的位图显示函数。我们下面将看到,StretchDIBits 函数没有这项功能,所以无法用 StretchDIBits 按不同像素大小显示 DIB。要实现此功能,你只能多次调用 StretchDIBits,每次都改变 BITMAPINFOHEADER
结构的信息,然后把结果显示在屏幕上的不同区域。
下面所示的 SEQDISP 程序说明了如何使用此项功能。
SEQDISP.C 程序里的所有文件 I/O 操作都发送在处理文件打开命令时。在 WM_COMMAND 消息处理的最后,程序进入一个循环,此循环读入每个像素行,然后用 SetDIBitsToDevice 显示这些像素行。整个 DIB 被保留在内存中,因此在 WM_PAINT 消息处理中也能显示它。
通过缩小或拉伸一个 DIB,可以在输出设备上按特定大小显示它,为此你可以使用 StretchDIBits:
该函数的参数和 SetDIBitsToDevice 一样,但有三点例外:
目标坐标包含逻辑宽度(cxDst)、逻辑高度(cyDst)和起始点。
此函数并不能通过顺序显示 DIB 来减少内存需求。
最后一个参数是光栅操作,它指出了 DIB 的像素怎样和输出设备的像素结合起来。在第 14 章里我们学习过这些光栅操作。目前我们把这个参数设置成 SRCCOPY。
这里还有一个更微妙的区别。如果你看一下 SetDIBitsToDevice 的定义,就会发现 cxSrc 和 cySrc 是 DWORD 类型——32 位无符号长整数类型。而 StretchDIBits 里的 cxSrc 和 cySrc(以及 cxDst 和 cyDst)则被定义成有符号整数,即它们的值可以是负数。事实上它们的值也确实是可以取负数的,很快我们就会看到。如果你为此开始检查其他的参数是否可以是负数,那么我来澄清一点:在这两个函数里,xSrc
和 ySrc 被定义成 int 值,但是这其实是错的。这些值总是非负数。
下表给出了 DIB 里的源矩形到目标矩形的映射。
右列里的 -1 并不很准确,因为拉伸(以及映射模式和其他变换)的程度可能会导致结果有些不同。
举个例子,假设有个 2 * 2 的 DIB,StretchDIBits 的 xSrc 和 ySrc 参数值都是 0,cxSrc 和 cySrc 的值都是 2。假设我们要用 MM_TEXT 映射模式把 DIB 显示到设备环境,并且不用任何变换。如果 xDst 和 yDst 都是 0,并且 cxDst 和 cyDst 都是 4,那么我们就是以两倍大小来拉伸 DIB。每个源像素(x, y)将会映射到四个目标像素,具体如下所示:
上表正确给出了目标矩形的四个角,即(0, 3),(3, 3),(0, 0)和(3, 0)。在其他情况下,坐标可能仅仅是近似的值。
仅当 xDst 和 yDst 是逻辑坐标时,SetDIBitsToDevice 才会受目标设备环境的映射模式的影响。而 StretchDIBits 则完全受映射模式的影响。例如,如果将度量映射模式设置为 y 值随着向上显示而增加,那么 DIB 就会被上下颠倒显示。
要想避免类似问题,可以把 cyDst 设成负数。事实上,可以把任何宽度和高度参数设成负数,使得 DIB 沿水平方向或垂直方向翻转。在 MM_TEXT 映射模式下,如果 cySrc 和 cyDst 的符号相反,则 DIB 会沿水平轴翻转,看起来就是上下颠倒的。如果 cxSrc 和cxDst 的符号相反,则 DIB 会沿垂直轴翻转,看起来就是一个镜像。
我们可以用两个表达式来对此进行总结。在这两个表达式中,xMM 和 yMM 给出了映射模式的取向:如果 x 随着从左到右显示而增加,xMM 就是 1;如果 x 随着从右到左显示而增加,xMM 是 -1;同样,如果 y 随着从上到下显示而增加,yMM 就是 1;如果 y 随着从下到上显示而增加,yMM 就是 -1。Sign 函数对正数返回 TRUE,对负数返回 FALSE:
下面所示的 SHOWDIB2 程序按 DIB 的实际大小显示它,并且把它拉伸到客户区窗口的大小,接着打印 DIB,并把 DIB 传输到剪贴板。
这里值得注意的是 ShowDib 函数,它根据菜单选项,按四种不同的方式在程序的客户区显示 DIB。SetDIBitsToDevice 函数可以把 DIB 显示在客户区的左上角,或者也可以把它显示在客户区的中间。使用 StretchDIBits 函数,也可以安两种方式显示 DIB:DIB 可以被拉伸到填满客户区,在这种情况下 DIB 可能会变形;或者各个方向都按相同比例拉伸,也就是说,DIB 不会变形。
把 DIB 复制到剪贴板的过程包括在全局共享内存中复制一份紧凑格式的 DIB 内存块。相应的剪贴板数据类型是 CF_DIB。程序没有给出怎样从剪贴板中复制 DIB。这是因为在只有一个指向紧凑 DIB 内存块的指针的情况下,要确定像素位的位移需要有更多的处理逻辑。在第 16 章结束前我将会告诉你该怎样做。
你可能还注意到了 SHOWDIB2 里的其他一些不足。如果在 256 色视频模式下运行 Windows,除了单色或 4 位 DIB,在显示其他 DIB 时你将会发现问题。颜色不能被正确显示。访问那些颜色需要使用调色板,这将在第 16 章介绍。你可能也会注意到速度的问题,尤其是在 Windows NT 下运行 SHOWDIB2 时。在第 16 章介绍 DIB 和位图时,我会介绍怎样处理这些问题。而且我还会在显示
DIB 时加入滚动条,如此一来,我们就可以查看实际大小比屏幕大的 DIB。
SetDIBitsToDevice 和 StretchDIBits 函数调用过程中,必须把每个像素(可能有几百万个)从设备无关格式转换成设备相关格式。
在许多情况,这个转换工作量并不大。例如,如果想在一个 24 位视频显示器上显示一个 24 位的 DIB,显示驱动程序最多只需要交换红、绿、蓝字节的顺序。在一个 24 位设备上显示一个 16 位的 DIB,则需要移动和填充一些像素位。在一个 16 位设备上显示一个 24 位的 DIB,需要移动和截断一些像素位。在一个
24 位设备上显示一个 4 位或 8 位的 DIB,需要在 DIB 的颜色表里查找 DIB 像素位,然后有可能需要把一些字节重新排列。
但如果想在一个 4 位或者 8 位的视频显示器上显示 16 位、24 位甚至是 32 位的 DIB,会发生什么情况呢?与之前不同,这需要彻底的颜色转换。设备驱动程序得在像素和所有能显示出来的颜色中,为 DIB 中的每个像素寻找一个最接近的颜色。这个过程涉及循环和计算。(GDI 函数 GetNearestColor 可以搜索最相近的颜色。)
整个三维 RGB 颜色矩阵可以用一个立方体来表示。而该曲面中任意两点的距离为:
其中,两种颜色分别是 R1G1B1 和 R2G2B2。搜索最相近的颜色涉及寻找从一种颜色到其他颜色集合的最短路径。幸运的是,在比较 RGB 颜色立方体中的距离时,不需要计算平方根。但是我们必须把每个将进行颜色转换的像素和设备的所有颜色进行比较,以便找出最接近像素的设备颜色。这个工作量仍然不小。(尽管在一个 8 位的设备上显示 8 位的 DIB 也设计最近颜色搜索,但这个操作不是对每个像素都必须做的;你仅仅需要对
DIB 颜色表中的每种颜色做最相近颜色搜索。)
因此,我们应该避免用 SetDIBitsToDevice 或 StretchDIBits 函数在 8 位视频显示适配器上显示 16 位、24 位或 32 位 DIB。这些 DIB 应该被转换成 8 位 DIB,或者要想性能更好的话,转换成 8 位 DDB。事实上,通过把 DIB 转换成 DDB,再使用 BitBlt 和 StretchBlt 把图像显示出来,我们可以加快几乎所有大小的 DIB 显示过程。
如果在一个 8 位视频显示器上运行 Windows(或者你因为想知道显示全彩 DIB 时的性能差异而刚刚切换到 8 位模式),你会发现另一个问题:并不是 DIB 的所有颜色都能被显示出来。任何在 8 位视频显示器上显示的 DIB 被限定成只有 20 种颜色。要想使用 20 种以上的颜色,你需要用到调色板管理器,详情请参见第
16 章。
最后,如果在一台机器上同时运行 Windows 98 和 Windows NT,你可能会注意到在用类似的视频模式显示很大的 DIB 时,Windows NT 需要的时间更长。这是 Windows NT 的客户、服务器体系结构造成的,在这种体系结构下,通过 API 传输大量数据需要付出速度的代价。解决方案仍然是把 DIB 转换成 DDB。此外,后面即将介绍的 CreateDIBSection 函数也是针对这种情况专门设计的。
位图是显示给用户看的。在本节,我们先来看一下 Windows 支持的两个函数,它们可以把 DIB 显示到视频显示器或打印页上。如果想追求更好的性能,最终你可能更愿意采取一种不太直接的方法来显示位图,我将在本章的后面进行讨论。这两个函数只是一个开端。
这两个函数分别是 SetDIBitsToDevice 和 StretchDIBits。每个函数都使用存储在内存中的 DIB,并且可以显示整个 DIB,或者 DIB 的部分矩形区域。使用 SetDIBitToDevice 时,所显示图像的像素大小和 DIB 的像素数一样。例如,一个 640 * 480 的 DIB 会占据整个标准 VGA 屏幕,但是在 300dpi 的激光打印机上,这个 DIB 只有 2.1 * 1.6 英寸。StretchDIBits 函数能通过拉伸或缩小 DIB 的行数和列数,把它按照特定大小显示到输出设备。
15.2.1 探究 DIB
在调用以上两个函数之一显示 DIB 时,需要图像的若干信息。之前我提过,DIB 文件包含下列部分:DIB 文件可以加载到内存中。如果整个文件(除了文件头)都存储在一个连续的内存块中,则指向此内存块开头的指针就被称为指向紧凑 DIB 格式,如下图所示:
通过剪贴板传输 DIB 时你就需要这种格式。从 DIB 创建一个画刷也同样需要这种格式。紧凑 DIB 格式使 DIB 能方便地存储在内存中,因为整个 DIB 被单个指针引用(例如,pBackedDib 指针),而这个指针可以像 BYTE 类型的指针一样定义。根据本章早前所展示的结构定义,你可以获取
DIB 存储的所有信息,包括颜色表和单个像素位。
然而,要得到这么多信息,需要写若干行代码。例如,想获取 DIB 的像素宽度,不能简单地使用下面的语句:
iWidth = ((PBITMAPINFOHEADER) pPackedDib)->biWidth;DIB 有可能是 OS/2 兼容格式。如果是那种格式,紧凑 DIB 会以 BITMAPCOREHEADER 结构开头,而且 DIB 的像素宽度和高度存储在 16 位 WORD 里,而不是 32 位 LONG 型变量。所以,首先你得检查 DIB 是否是旧格式的 DIB,然后再进行相应操作:
if (((PBITMAPCOREHEADER) pBackedDib)->bcSize == sizeof(BITMAPCOREHEADER)) iWidth = ((PBITMAPCOREHEADER) pPackedDib)->biWidth; else iWidth = ((PBITMAPINFOHEADER) pPackedDib)->biWidth显然,这不是那么糟糕,但是也肯定不像我们期望的那样简洁明了。
现在让我们做一个有趣的练习:给定一个指向紧凑 DIB 的指针,请找出坐标点(5, 27) 的像素值。即使假设 DIB 不是 OS/2 兼容格式,也需要知道 DIB 的宽度、高度和每像素的位数。需要计算每个像素行的字节长度。还需要确定颜色表的元素个数,以及颜色表是否包含三个 32 位颜色遮罩。此外,还要检查 DIB 是否已经被压缩,如果是,就不能直接访问像素。
如果想直接访问 DIB 的所有像素(就像进行图像处理时做的一样),上述做法可能会增加不少处理时间。因此,尽管维护一个指向紧凑 DIB 的指针很方便,但是这不代表能写出有效率的代码。绝佳的解决方案就是为 DIB 定义一个 C++ 类,它包含足够多的成员数据,使你能快速随机地访问 DIB 像素。然而,在本书开始,我才鞥允诺过你不需要知道任何 C++ 知识,所以我会在第 16 章给出 C
版本的解决方案。
对于 SetDIBitsToDevice 和 StretchDiBits 函数,你需要知道的信息包括一个指向 DIB 的 BITMAPINFO 结构的指针。你可能还记得,BITMAPINFO 结构由 BITMAPINFOHEADER 结构和颜色表组成。所以,这其实就是一个经过适当的强制转换的指向紧凑 DIB 的简单指针。
这两个函数还要求有像素位的指针。这可以从信息头里的信息得到,只是代码不漂亮而已。注意,如果你能获取 BITMAPFILEHEADER 结构的 bfOffBits 字段,计算指针就简单一些。bfOffBits 字段指示了从 DIB 文件开头到像素位的位移量。你可以简单地把这段位移加到 BITMAPINFO 指针上,再减去 BITMAPFILEHEADER 结构的大小。但是,如果指针指向的是剪贴板的紧凑
DIB,这种方法是行不通的,因为你没有 BITMAPFILEHEADER 结构。
下图给出了需要用到的两个指针:
SetDIBitsToDevice 和 StretchDIBits 函数需要两个指向 DIB 的指针,因为它们指向的两个部分不一定在一段连续的内存块中。可能有以下两个内存块:
事实上,像这样把一个 DIB 分成两个内存块存放非常有用。目前我们讨论的仅仅是紧凑 DIB,所以才把整个 DIB 存放在单个内存块中。
除了这两指针,SetDIBitsToDevice 和 StretchDIBits 函数也需要知道 DIB 有多少像素宽、多少像素高。如果只显示 DIB 的一部分,就不需要知道它们的具体值,但如果需要在 DIB 像素位数组中定义一个矩形,则他们就确定了矩形的宽和高的上限。
15.2.2 从像素到像素
SetDIBitsToDevice 函数会显示一个没有任何拉伸和压缩的 DIB。DIB 的每个像素分别对应于输出设备的像素。图像的方向总是被正确地显示,也就是说,图像的最顶行显示在最上面。任何可能影响设备环境的变换决定了 DIB 显示的实际起始位置,但是这种变换对于图像的大小或方向没有影响。以下是SetDIBitsToDevice 函数:
iLines = SetDIBitsToDevice( hdc, // device context handle xDst, // x destination coordinate yDst, // y destination coordiante cxSrc, // source rectangle width cySrc, // source rectangle height xSrc, // x source coordinate ySrc, // y source coordinate yScan, // first scan line to draw cyScans, // number of scan lines to draw pBits, // pointer to DIB pixel bits pInfo, // pointer to DIB information fClrUse); // color use flag别因为函数参数的个数而烦恼。在多数情况下,这个函数比它看起来要容易使用。当然,对于有些情况,用起来会比较复杂。但是我们是可以解决的。
和 GDI 显示函数一样,SetDIBitsToDevice 的第一个参数是指向设备环境的句柄,它指明了你想要在什么设备上显示 DIB。接下来的两个参数,xDst 和 yDst,是输出设备的逻辑坐标点,指明了 DIB 图像的左上角将在什么坐标上显示。(这里左上角的“上”指的是图像的视觉上方,而不是 DIB 的第一个像素行。)注意,这两个参数是逻辑坐标,所以它们受实际映射模式的限制,或者——在 Windows NT 下——就是你可能设置的任何变换。在默认 MM_TEXT 映射模式下,你应该把这两个参数都设为
0,以便从显示表面的左上端起显示 DIB 图像。
你可以显示整个 DIB 图像,也可以只显示一部分。这就是接下来四个参数的作用。有时这几个参数的使用会造成这种情况:DIB 像素数据的上下倒置带来现实中的扭曲。我将很快给予讨论。暂时记住,要显示整个 DIB,则应把 xSrc 和 ySrc 设成 0,cxSrc 和 cySrc 分别等于 DIB 的像素宽度和高度。注意,对于自上而下的DIB,BITMAPINFOHEADER 结构的 biHeight 字段是负数,所以 cySrc 应该设置成 biHeight 字段的绝对值。
这个函数的 MSDN 文档(/Platform SDK/Graphics and Multimedia Services/GDI/Bitmaps/Bitmap Reference/Bitmap Functions/SetDIBitsToDevice)(译注:/MSDN Library/Win32 and Com Development/Graphics and Multimedia/Windows
GDI/Bitmaps Reference/Bitmap Functions)介绍说 xSrc, ySrc, cxSrc, cySrc 参数是逻辑单位。这并不正确。它们是像素坐标点和像素尺寸。对 DIB 里的像素讨论逻辑坐标点和单位是没有意义的。另外,无论映射模式是什么,在输出设备上显示的 DIB 总是 cxSrc 个像素宽,cySrc 个像素高。
我将暂时跳过对接下来两个参数 yScan 和 cyScan 的详细讨论。在从磁盘文件或从一个拨号网络连接上读取像素位的时候,这两个参数通过按顺序每次显示 DIB 的一部分,来减少显示 DIB 所需的内存。通常,yScan 设成 0,cyScan 设成 DIB 的高度。
pBits 参数是一个指向 DIB 像素位的指针。pInfo 参数是一个指向 DIB 的 BITMAPINFO 结构的指针。尽管 BITMAPINFO 结构的地址和 BITMAPINFOHEADER 结构的地址一样,但 SetDIBitsToDevice 函数被定义成使用 BITMAPINFO 结构,这是由于:对于 1 位、4 位和 8 位 DIB 来说,位图信息头后面必须跟有一个颜色表。虽然
pInfo 参数定义成指向 BITMAPINFO 结构的指针,它也可以是指向 BITMAPCOREINFO,BITMAPV4HEADER 或者 BITMAPV5HEADER 结构的指针。
最后一个参数可以是 DIB_RGB_COLORS,或者是 DIB_PAL_COLORS,它们在 WINGDI.H 中分别定义为 0 和 1。你暂时会使用 DIB_RGB_COLORS,即表明 DIB 带有颜色表。DIB_PAL_COLORS 标识意味着 DIB 里的颜色表将被由设备环境选中并实现的 16 位索引的逻辑调色板取代。第 16 章将介绍这个选项。现在,可以只用 DIB_RGB_COLORS,或者如果你比较懒,就简单地用
0。
SetDIBitsToDevice 函数返回的是它显示的扫描行的个数。
综上所述,为了调用 SetDIBitsToDevice 以显示整个 DIB 图像,需要以下信息。
hdc 指向目标设备的设备环境句柄。
xDst 和 yDst 图像左上角的目标坐标点。
cxDib 和 cyDib DIB 的像素宽度和高度,cyDib 是 BITMAPINFOHEADER 结构的 biHeight 字段的绝对值。
pInfo 和 pBits 指向位图信息部分和像素位的指针。
然后可以如下调用 SetDIBitsToDevice:
SetDIBitsToDevice( hdc, // device context handle xDst, // x destination coordinate yDst, // y destination coordiante cxDib, // source rectangle width cyDib, // source rectangle height 0, // x source coordinate 0, // y source coordinate 0, // first scan line to draw cyDib, // number of scan lines to draw pBits, // pointer to DIB pixel bits pInfo, // pointer to DIB information 0); // color use flag这样,DIB 的 13 个参数里,有 4 个通常设成 0,其他参数则重复使用。
下面所示的 SHOWDIB1 程序通过使用 SetDIBitsToDevice 函数显示一个 DIB。
/*------------------------------------------------- SHOWDIB1.C -- Shows a DIB in the client area (c) Charles Petzold, 1998 --------------------------------------------------*/ #include <windows.h> #include "dibfile.h" #include "resource.h" LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); TCHAR szAppName[] = TEXT("ShowDib1"); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevinstance, PSTR szCmdLine, int iCmdShow) { HACCEL hAccel; HWND hwnd; MSG msg; WNDCLASS wndclass; wndclass.style = CS_HREDRAW | CS_VREDRAW; wndclass.lpfnWndProc = WndProc; wndclass.cbClsExtra = 0; wndclass.cbWndExtra = 0; wndclass.hInstance = hInstance; wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION); wndclass.hCursor = LoadCursor(NULL, IDC_ARROW); wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); wndclass.lpszMenuName = szAppName; wndclass.lpszClassName = szAppName; if (!RegisterClass(&wndclass)) { MessageBox(NULL, TEXT("This program requires Windows NT!"), szAppName, MB_ICONERROR); return 0; } hwnd = CreateWindow(szAppName, TEXT("Show DIB #1"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL); ShowWindow(hwnd, iCmdShow); UpdateWindow(hwnd); hAccel = LoadAccelerators(hInstance, szAppName); while (GetMessage(&msg, NULL, 0, 0)) { if (!TranslateAccelerator(hwnd, hAccel, &msg)) { TranslateMessage(&msg); DispatchMessage(&msg); } } return msg.wParam; } LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static BITMAPFILEHEADER * pbmfh; static BITMAPINFO * pbmi; static BYTE * pBits; static int cxClient, cyClient, cxDib, cyDib; static TCHAR szFileName[MAX_PATH], szTitleName[MAX_PATH]; BOOL bSuccess; HDC hdc; PAINTSTRUCT ps; switch (message) { case WM_CREATE: DibFileInitialize(hwnd); return 0; case WM_SIZE: cxClient = LOWORD(lParam); cyClient = HIWORD(lParam); return 0; case WM_INITMENUPOPUP: EnableMenuItem((HMENU)wParam, IDM_FILE_SAVE, pbmfh ? MF_ENABLED : MF_GRAYED); return 0; case WM_COMMAND: switch (LOWORD(wParam)) { case IDM_FILE_OPEN: // Show the File Open dialog box if (!DibFileOpenDlg(hwnd, szFileName, szTitleName)) return 0; // If there's an existing DIB, free the memory if (pbmfh) { free(pbmfh); pbmfh = NULL; } // Load the entire DIB into memory SetCursor(LoadCursor(NULL, IDC_WAIT)); ShowCursor(TRUE); pbmfh = DibLoadImage(szFileName); ShowCursor(FALSE); SetCursor(LoadCursor(NULL, IDC_ARROW)); // Invalidate the client area for later update InvalidateRect(hwnd, NULL, TRUE); if (pbmfh == NULL) { MessageBox(hwnd, TEXT("Cannot load DIB file"), szAppName, 0); return 0; } // Get pointers to the info structure & the bits pbmi = (BITMAPINFO *)(pbmfh + 1); pBits = (BYTE *)pbmfh + pbmfh->bfOffBits; // Get the DIB width and height if (pbmi->bmiHeader.biSize == sizeof(BITMAPCOREHEADER)) { cxDib = ((BITMAPCOREHEADER *)pbmi)->bcWidth; cyDib = ((BITMAPCOREHEADER *)pbmi)->bcHeight; } else { cxDib = pbmi->bmiHeader.biWidth; cyDib = abs(pbmi->bmiHeader.biHeight); } return 0; case IDM_FILE_SAVE: // Show the File Save dialog box if (!DibFileSaveDlg(hwnd, szFileName, szTitleName)) return 0; // Save the DIB to memory SetCursor(LoadCursor(NULL, IDC_WAIT)); ShowCursor(TRUE); bSuccess = DibSaveImage(szFileName, pbmfh); ShowCursor(FALSE); SetCursor(LoadCursor(NULL, IDC_ARROW)); if (!bSuccess) MessageBox(hwnd, TEXT("Cannot save DIB file"), szAppName, 0); return 0; } break; case WM_PAINT: hdc = BeginPaint(hwnd, &ps); if (pbmfh) SetDIBitsToDevice(hdc, 0, // xDst 0, // yDst cxDib, // cxSrc cyDib, // cySrc 0, // xSrc 0, // ySrc 0, // first scan line cyDib, // number of scan lines pBits, pbmi, DIB_RGB_COLORS); EndPaint(hwnd, &ps); return 0; case WM_DESTROY: if (pbmfh) free(pbmfh); PostQuitMessage(0); return 0; } return DefWindowProc(hwnd, message, wParam, lParam); }
/*------------------------------------------- DIBFILE.H -- Header File for DIBFILE.C -------------------------------------------*/ void DibFileInitialize(HWND hwnd); BOOL DibFileOpenDlg(HWND hwnd, PTSTR pstrFileName, PTSTR pstrTitleName); BOOL DibFileSaveDlg(HWND hwnd, PTSTR pstrFileName, PTSTR pstrTitleName); BITMAPFILEHEADER * DibLoadImage(PTSTR pstrFileName); BOOL DibSaveImage(PTSTR pstrFileName, BITMAPFILEHEADER *);
/*------------------------------------------- DIBFILE.C -- DIB File Functions -------------------------------------------*/ #include <windows.h> #include <commdlg.h> #include "dibfile.h" static OPENFILENAME ofn; void DibFileInitialize(HWND hwnd) { static TCHAR szFilter[] = TEXT("Bitmap Files (*.MBP)\0*.bmp\0") TEXT("All Files (*.*)\0*.*\0\0"); ofn.lStructSize = sizeof(OPENFILENAME); ofn.hwndOwner = hwnd; ofn.hInstance = NULL; ofn.lpstrFilter = szFilter; ofn.lpstrCustomFilter = NULL; ofn.nMaxCustFilter = 0; ofn.nFilterIndex = 0; ofn.lpstrFile = NULL; // Set int Open and Close functions ofn.nMaxFile = MAX_PATH; ofn.lpstrFileTitle = NULL; // Set in Open and Close functions ofn.nMaxFileTitle + MAX_PATH; ofn.lpstrInitialDir = NULL; ofn.lpstrTitle = NULL; ofn.Flags = 0; ofn.nFileOffset = 0; ofn.nFileExtension = 0; ofn.lpstrDefExt = TEXT("bmp"); ofn.lCustData = 0; ofn.lpfnHook = NULL; ofn.lpTemplateName = NULL; } BOOL DibFileOpenDlg(HWND hwnd, PTSTR pstrFileName, PTSTR pstrTitleName) { ofn.hwndOwner = hwnd; ofn.lpstrFile = pstrFileName; ofn.lpstrFileTitle = pstrTitleName; ofn.Flags = 0; return GetOpenFileName(&ofn); } BOOL DibFileSaveDlg(HWND hwnd, PTSTR pstrFileName, PTSTR pstrTitleName) { ofn.hwndOwner = hwnd; ofn.lpstrFile = pstrFileName; ofn.lpstrFileTitle = pstrTitleName; ofn.Flags = OFN_OVERWRITEPROMPT; return GetSaveFileName(&ofn); } BITMAPFILEHEADER * DibLoadImage(PTSTR pstrFileName) { BOOL bSuccess; DWORD dwFileSize, dwHighSize, dwBytesRead; HANDLE hFile; BITMAPFILEHEADER * pbmfh; hFile = CreateFile(pstrFileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL); if (hFile == INVALID_HANDLE_VALUE) return NULL; dwFileSize = GetFileSize(hFile, &dwHighSize); if (dwHighSize) { CloseHandle(hFile); return NULL; } pbmfh = (PBITMAPFILEHEADER)malloc(dwFileSize); if (!pbmfh) { CloseHandle(hFile); return NULL; } bSuccess = ReadFile(hFile, pbmfh, dwFileSize, &dwBytesRead, NULL); CloseHandle(hFile); if (!bSuccess || (dwBytesRead != dwFileSize) || (pbmfh->bfType != *(WORD *) "BM") || (pbmfh->bfSize != dwFileSize)) { free(pbmfh); return NULL; } return pbmfh; } BOOL DibSaveImage(PTSTR pstrFileName, BITMAPFILEHEADER * pbmfh) { BOOL bSuccess; DWORD dwBytesWritten; HANDLE hFile; hFile = CreateFile(pstrFileName, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) return FALSE; bSuccess = WriteFile(hFile, pbmfh, pbmfh->bfSize, &dwBytesWritten, NULL); CloseHandle(hFile); if (!bSuccess || (dwBytesWritten != pbmfh->bfSize)) { DeleteFile(pstrFileName); return FALSE; } return TRUE; }
SHOWDIB1.RC (excerpts) // Microsoft Visual C++ generated resource script. // #include "resource.h" ///////////////////////////////////////////////////////////////////////////// // // Menu // SHOWDIB1 MENU BEGIN POPUP "&File" BEGIN MENUITEM "&Open...", IDM_FILE_OPEN MENUITEM "&Save...", IDM_FILE_SAVE END END
RESOURCE.H (excerpts) // Microsoft Visual C++ 生成的包含文件。 // 供 ShowDib1.rc 使用 // #define IDM_FILE_OPEN 40001 #define IDM_FILE_SAVE 40002
DIBFILE.C 文件含有显示打开文件和保存文件对话框的例程,也包含把整个 DIB 文件(带有完整的 BITMAPFILEHEADER 结构)加载到单个内存块的例程。此程序也能把这样的内存块写到文件中。
SHOWDIB1.C 在处理文件打开命令时加载 DIB 文件,然后此程序计算 BITMAPINFOHEADER 结构的位移和内存块中的像素位。此程序还能获取 DIB 的像素宽度和高度。所有这些信息存储在一些静态变量里。处理 WM_PAINT 消息时,程序通过调用 SetDIBitsToDevice 显示 DIB。
当然,SHOWDIB1 缺少了一些功能。例如,对于客户区而言,DIB 如果太大,图像就没法通过滚动条来滚屏。这些不足将在第 16 章结束前弥补。
15.2.3 DIB 的颠倒世界
我们将学到一个重要的教训,不仅在生活中,而且在设计操作系统的应用程序编程接口时你都能得到这个教训,即:如果你一开始弄糟了某件事,后来在你试图解决问题时只会更糟。回顾一下过去,OS/2 Presentation Manager 中的 DIB 像素位是按由下而上的方向定义的,这种定义在某种程度上有些道理,因为在 PM 中的每个对象都以默认左下角为源点。例如,在 PM 窗口中,默认的(0, 0)原点是窗口的左下角。(如果觉得这比较古怪,说明你还是个普通人。如果不觉得有什么不妥,那你可能就是一个数学家。)位图绘制函数也以左下标点来指定目标位置。
因此,如果在 OS/2 中为位图指定了(0, 0)为目标坐标点,那么图像就会如图 15-3 所示显示在窗口的左下端。
在足够慢的机器上,你也许还能看到位图是从下而上绘制的。
图 15-3 在 OS/2 下显示的以(0, 0)为目标的位图
尽管 OS/2 坐标系可能看起来很奇怪,但它的优点是一致性。位图的(0, 0)原点总是位图文件第一行的第一个像素,并且此像素被映射到位图绘制函数给出的目标坐标中。
Windows 的问题是它不能保持内部的一致性。如果想显示整个 DIB 图像里的一个矩形子集,会使用参数 xSrc,ySrc,csSrc 和 cySrc。这些源坐标和大小是相对于 DIB 的第一行数据而言的,也就是图像的最底行。这跟 OS/2 很像。但是,与之不同的是, Windows 在目标坐标上显示图像的最顶行。因此,如果要显示整个 DIB 图像,在(xDst, yDst)坐标点显示的像素是位于(0, cyDib-1)坐标点的 DIB 像素。这是 DIB 数据的最后一行,但是是图像的顶行。如果只显示图像的一部分,在(xDst,
yDst)显示的像素是位于(xSrc, ySrc+cySrc-1)的 DIB 像素。
图 15-4 显示的图能帮你明白坐标的对应关系。下面给出的 DIB 按照你可能设想的方式在内存中存储——即上下倒置。度量坐标系的原点与 DIB 像素数据的第一位一致。SetDIBitsToDevice 的 xSrc 参数从 DIB 左边开始度量,cxSrc
是到 xSrc 右边的图像宽度。这很直接明了。ySrc 参数从 DIB 数据的第一行(即图像底部)开始度量,cySrc 是从 ySrc 到最后一行数据(图像顶部0的图像高度。
图 15-4 常规(自下而上)DIB 的 DIB 坐标说明
如果目标设备环境使用了默认的 MM_TEXT 映射模式的像素坐标,则源矩形和目标矩形的顶点坐标关系见下表:
源 矩 形 | 目标矩形 |
---|---|
(xSrc, ySrc) | (xDst, yDst + cySrc - 1) |
(xSrc + cxSrc - 1, ySrc) | (xDst + cxSrc - 1, yDst + cySrc - 1) |
(xSrc, ySrc + cySrc - 1) | (xDst, yDst) |
(xSrc + cxSrc - 1, ySrc + cySrc - 1) | (xDst + cxSrc - 1, yDst) |
到目前为止,我们讨论了 BITMAPINFOHEADER 结构的 biHeight 字段取正值的正常情况。如果 biHeight 字段为负值,DIB 数据会按照合理的自上而下的方式排列。你可能会认为着解决了所有问题。如果是这样,只能说你太过天真了。
很明显,某些人会认为如果翻转一个自上而下排列的 DIB 的所有行,再把 biHeight 字段设成正值,结果应该跟正常的自下而上的 DIB 一样。设计 DIB 的矩形区域的已有代码不需要作任何改变。我认为这个想法是合理的,但是它忽视了一个现实:程序仍然需要被修改,以便在处理自上而下的 DIB 时避免使用负的高度值。
此外,这种做法的结果带有奇怪的含义。它意味着自上而下 DIB 的源坐标有一个位于 DIB 最后一个数据行的原点,也就是图像的最底行。这和我们遇到过的任何情况都不同。位于(0, 0)原点的 DIB 像素不再是 pBits 指针引用的第一个像素,也不是 DIB
文件的最后一个像素,而是处于中间的某个位置。
图 15-5 展示了怎样在自上而下的 DIB 中指定一块矩形区域。同样,图示的 DIB 代表了它在文件或内存中的实际存储方式。
图 15-5 自上而下 DIB 的 DIB 坐标说明
不管怎样,这种策略的实际优势在于 SetDIBitsToDevice 函数的参数与 DIB 数据取向无关。如果你有两个 DIB(一个自下而上,另一个自上而下)显示同一幅图像(即两个 DIB 文件的行排列顺序相反),你可以用相同的 SetDIBitsToDevice 函数参数显示图像的同一部分。
下面所示的 APOLLO11 程序演示了这一点。
/*------------------------------------------------- APOLLO11.C -- Program for screen captures (c) Charles Petzold, 1998 --------------------------------------------------*/ #include <windows.h> #include "dibfile.h" LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); TCHAR szAppName[] = TEXT("Apollo11"); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevinstance, PSTR szCmdLine, int iCmdShow) { HWND hwnd; MSG msg; WNDCLASS wndclass; wndclass.style = CS_HREDRAW | CS_VREDRAW; wndclass.lpfnWndProc = WndProc; wndclass.cbClsExtra = 0; wndclass.cbWndExtra = 0; wndclass.hInstance = hInstance; wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION); wndclass.hCursor = LoadCursor(NULL, IDC_ARROW); wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); wndclass.lpszMenuName = NULL; wndclass.lpszClassName = szAppName; if (!RegisterClass(&wndclass)) { MessageBox(NULL, TEXT("This program requires Windows NT!"), szAppName, MB_ICONERROR); return 0; } hwnd = CreateWindow(szAppName, TEXT("Apollo 11"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL); ShowWindow(hwnd, iCmdShow); UpdateWindow(hwnd); while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; } LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static BITMAPFILEHEADER * pbmfh[2]; static BITMAPINFO * pbmi[2]; static BYTE * pBits[2]; static int cxClient, cyClient, cxDib[2], cyDib[2]; HDC hdc; PAINTSTRUCT ps; switch (message) { case WM_CREATE: pbmfh[0] = DibLoadImage(TEXT("Apollo11.bmp")); pbmfh[1] = DibLoadImage(TEXT("ApolloTD.bmp")); if (pbmfh[0] == NULL || pbmfh[1] == NULL) { MessageBox(hwnd, TEXT("Cannot load DIB file"), szAppName, 0); return 0; } // Get pointers to the info structure & the bits pbmi[0] = (BITMAPINFO *)(pbmfh[0] + 1); pbmi[1] = (BITMAPINFO *)(pbmfh[1] + 1); pBits[0] = (BYTE *)pbmfh[0] + pbmfh[0]->bfOffBits; pBits[1] = (BYTE *)pbmfh[1] + pbmfh[1]->bfOffBits; // Get the DIB width and height (assume BITMAPINFOHEADER) // Note that cyDib is the absolute value of the header value!! cxDib[0] = pbmi[0]->bmiHeader.biWidth; cxDib[1] = pbmi[1]->bmiHeader.biWidth; cyDib[0] = abs(pbmi[0]->bmiHeader.biHeight); cyDib[1] = abs(pbmi[1]->bmiHeader.biHeight); return 0; case WM_SIZE: cxClient = LOWORD(lParam); cyClient = HIWORD(lParam); return 0; case WM_PAINT: hdc = BeginPaint(hwnd, &ps); // Bottom-up DIB full size SetDIBitsToDevice(hdc, 0, // xDst cyClient / 4, // yDst cxDib[0], // cxSrc cyDib[0], // cySrc 0, // xSrc 0, // ySrc 0, // first scan line cyDib[0], // number of scan lines pBits[0], pbmi[0], DIB_RGB_COLORS); // Bottom-up DIB partial SetDIBitsToDevice(hdc, 240, // xDst cyClient / 4, // yDst 80, // cxSrc 166, // cySrc 80, // xSrc 60, // ySrc 0, // first scan line cyDib[0], // number of scan lines pBits[0], pbmi[0], DIB_RGB_COLORS); // Top-down DIB full size SetDIBitsToDevice(hdc, 340, // xDst cyClient / 4, // yDst cxDib[1], // cxSrc cyDib[1], // cySrc 0, // xSrc 0, // ySrc 0, // first scan line cyDib[1], // number of scan lines pBits[1], pbmi[1], DIB_RGB_COLORS); // Top-down DIB partial SetDIBitsToDevice(hdc, 580, // xDst cyClient / 4, // yDst 80, // cxSrc 166, // cySrc 80, // xSrc 60, // ySrc 0, // first scan line cyDib[1], // number of scan lines pBits[1], pbmi[1], DIB_RGB_COLORS); EndPaint(hwnd, &ps); return 0; case WM_DESTROY: if (pbmfh[0]) free(pbmfh[0]); if (pbmfh[1]) free(pbmfh[1]); PostQuitMessage(0); return 0; } return DefWindowProc(hwnd, message, wParam, lParam); }
这个程序加载了两个 DIB,分别是 APOLLO11.BMP(自下而上的版本)和 APOLLOTD.BMP(自上而下的版本)。两个 DIB 都是 220 个像素宽,240 个像素高。注意,程序在根据头信息结构确定 DIB 的宽度和高度时,使用了abs 函数来获得 biHeight 字段的绝对值。全部或部分显示 DIb 时,无论显示的是那幅位图,xSrc,ySrc,cxSrc 和 cySrc 坐标都相同。程序结果见图 15-7。
图 15-7 APOLLO11 的显示结果
注意,“第一行扫描行”和“扫描行个数”参数没有改变。我会很快讲解这两个参数。pBits 参数也保持不变。不要试图改变 pBits,让它仅仅指向你想要显示的 DIB 区域。
我在这个问题上花费了大量时间来讨论,并不是因为我想打击 Windows 开发者尽力整合有疑问的 APi 定义的努力,而是想让你在问题看起来费解时不那么紧张。这个问题是令人费解的,因为它本身就很乱。
我也想让你注意 Windows 文档里的某些语句,例如 SetDIBitsToDevice 文档中有这样一句话:“自下而上 DIB 的原点是位图的左下角:自上而下的 DIB 原点是左上角。”这不仅仅是模糊不清,而是完全错误的。我们可以像这样更好地表述二者的区别:自下而上DIB
的原点是位图图像的左下角,即位图数据的第一行的第一个像素行;自上而下的DIB 的原点也是位图图像的左下角,但在这种情况下,左下角是位图数据最后一行的第一个像素。
如果你想写一个函数,使程序访问 DIB 的单个像素位,情况将变得更糟。具体操作应该和显示部分 DIB 图像时指定坐标的方法保持一致。我的解决方案(我将在第 16 章的一个 DIB 库中实现)是引用 DIB 像素和坐标时,假想(0, 0)原点是 DIB 图像被正确显示时,你所见的 DIB 的顶行的最左边的像素。
15.2.4 顺序显示
如果你的内存足够大,编程就会变得比较容易。显示存于磁盘文件中的 DIB 可以分成两个独立步骤:把 DIB 加载到内存,然后显示。即便如此,有时你可能不想把整个文件加载到内存后再显示 DIB。即使有足够内存容纳 DIB,把 DIB 移到内存中也可能会迫使 Windows 虚拟内存系统把其他代码和数据移到磁盘。如果碰到 DIB 仅仅是被用来显示,之后又立即从内存中删除的情况,这样的操作会很令人痛苦。
另外一个问题是:假设 DIB 位于一个慢速的存储媒介例如软盘上,或者来自于调制解调器,或是来自于一个从扫描仪或视频抓帧器获取像素数据的转换例程。你想等到整个 DIB 被加载到内存后才显示它吗?还是你宁愿在从磁盘或电话线或扫描仪得到
DIB 后立即按正确的顺序显示它?
SetDIBitsToDevice 函数的 yScan 和 cyScans 参数就是用来解决这些问题的。要使用它们,你得多次调用 SetDIBitsToDevice,其中的大多数调用使用同样的参数。然而,在每次调用中,pBits 参数需指向整个位图像素矩阵的不同部分。yScan 参数指出了 pBits 指向哪个像素数据行,cyScans 参数是 pBits 引用的数据行的个数。这个方法在很大程度上减少了内存需求。你只需要给
DIB 的信息部分(BITMAPINFOHEADER 结构和颜色表)以及至少一个像素数据行分配足够的内存即可。
举例来说,假设 DIB 有 23 行像素。你想按 5 行一个数据显示 DIB。这就可能需要分配一块存储 DIB 的 BITMAPINFO 部分的内存,此内存被 pInfo 变量引用。然后从文件中读数据。检查了这个数据结构的字段后,你就可以计算每行有多少个字节。用这个结果乘以 5,根据得出的值分配另一个内存块(pBits)。这之后就可以读入前 5 行数据,像平时一样调用 SetDIBitsToDevice 函数,不过 yScan 设为 5。继续调用这个函数,把 yScan 设成 10,再设成
15.最终,把最后 3 行读入被 pBits 引用的内存块,然后调用 SetDIBitsToDevice 函数,把 yScan 设成 20,cyScans 设成 3。
现在有个坏消息。首先,通过这种方式使用 SetDIBitsToDevice 要求你的程序里数据获取和数据显示两者之间要高度耦合。这通常不太好,因为你必须在获取数据和显示数据之间切换。这样,整个过程会变慢。其次,SetDIBitsToDevice 是唯一有此功能的位图显示函数。我们下面将看到,StretchDIBits 函数没有这项功能,所以无法用 StretchDIBits 按不同像素大小显示 DIB。要实现此功能,你只能多次调用 StretchDIBits,每次都改变 BITMAPINFOHEADER
结构的信息,然后把结果显示在屏幕上的不同区域。
下面所示的 SEQDISP 程序说明了如何使用此项功能。
/*------------------------------------------------- SEQDISP.C -- Sequential Display of DIBs (c) Charles Petzold, 1998 --------------------------------------------------*/ #include <windows.h> #include "resource.h" LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); TCHAR szAppName[] = TEXT("SeqDisp"); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevinstance, PSTR szCmdLine, int iCmdShow) { HACCEL hAccel; HWND hwnd; MSG msg; WNDCLASS wndclass; wndclass.style = CS_HREDRAW | CS_VREDRAW; wndclass.lpfnWndProc = WndProc; wndclass.cbClsExtra = 0; wndclass.cbWndExtra = 0; wndclass.hInstance = hInstance; wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION); wndclass.hCursor = LoadCursor(NULL, IDC_ARROW); wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); wndclass.lpszMenuName = szAppName; wndclass.lpszClassName = szAppName; if (!RegisterClass(&wndclass)) { MessageBox(NULL, TEXT("This program requires Windows NT!"), szAppName, MB_ICONERROR); return 0; } hwnd = CreateWindow(szAppName, TEXT("DIB Sequential Display"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL); ShowWindow(hwnd, iCmdShow); UpdateWindow(hwnd); hAccel = LoadAccelerators(hInstance, szAppName); while (GetMessage(&msg, NULL, 0, 0)) { if (!TranslateAccelerator(hwnd, hAccel, &msg)) { TranslateMessage(&msg); DispatchMessage(&msg); } } return msg.wParam; } LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static BITMAPINFO * pbmi; static BYTE * pBits; static int cxDib, cyDib, cBits; static OPENFILENAME ofn; static TCHAR szFileName[MAX_PATH], szTitleName[MAX_PATH]; static TCHAR szFilter[] = TEXT("Bitmap Files (*.BMP)\0*.bmp\0") TEXT("All Files (*.*)\0*.*\0\0"); BITMAPFILEHEADER bmfh; BOOL bSuccess, bTopDown; DWORD dwBytesRead; HANDLE hFile; HDC hdc; HMENU hMenu; int iInfoSize, iBitsSize, iRowLength, y; PAINTSTRUCT ps; switch (message) { case WM_CREATE: ofn.lStructSize = sizeof(OPENFILENAME); ofn.hwndOwner = hwnd; ofn.hInstance = NULL; ofn.lpstrFilter = szFilter; ofn.lpstrCustomFilter = NULL; ofn.nMaxCustFilter = 0; ofn.nFilterIndex = 0; ofn.lpstrFile = szFileName; ofn.nMaxFile = MAX_PATH; ofn.lpstrFileTitle = szTitleName; ofn.nMaxFileTitle = MAX_PATH; ofn.lpstrInitialDir = NULL; ofn.lpstrTitle = NULL; ofn.Flags = 0; ofn.nFileOffset = 0; ofn.nFileExtension = 0; ofn.lpstrDefExt = TEXT("bmp"); ofn.lCustData = 0; ofn.lpfnHook = NULL; ofn.lpTemplateName = NULL; return 0; case WM_COMMAND: hMenu = GetMenu(hwnd); switch (LOWORD(wParam)) { case IDM_FILE_OPEN: // Display File Open dialog if (!GetOpenFileName(&ofn)) return 0; // Get rid of old DIB if (pbmi) { free(pbmi); pbmi = NULL; } if (pBits) { free(pBits); pBits = NULL; } // Generate WM_PAINT message to erase background InvalidateRect(hwnd, NULL, TRUE); UpdateWindow(hwnd); // Open the file hFile = CreateFile(szFileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL); if (hFile == INVALID_HANDLE_VALUE) { MessageBox(hwnd, TEXT("Cannot open file."), szAppName, MB_ICONWARNING | MB_OK); return 0; } // Read in the BITMAPFILEHEADER bSuccess = ReadFile(hFile, &bmfh, sizeof(BITMAPFILEHEADER), &dwBytesRead, NULL); if (!bSuccess || dwBytesRead != sizeof(BITMAPFILEHEADER)) { MessageBox(hwnd, TEXT("Cannot read file."), szAppName, MB_ICONWARNING | MB_OK); CloseHandle(hFile); return 0; } // Check that it's a bitmap if (bmfh.bfType != *(WORD *) "BM") { MessageBox(hwnd, TEXT("File is not a bitmap."), szAppName, MB_ICONWARNING | MB_OK); CloseHandle(hFile); return 0; } // Allocate memory for header and bits iInfoSize = bmfh.bfOffBits - sizeof(BITMAPFILEHEADER); iBitsSize = bmfh.bfSize - bmfh.bfOffBits; pbmi = (BITMAPINFO *)malloc(iInfoSize); pBits = (BYTE *)malloc(iBitsSize); if (pbmi == NULL || pBits == NULL) { MessageBox(hwnd, TEXT("Cannot allocate memory."), szAppName, MB_ICONWARNING | MB_OK); if (pbmi) free(pbmi); if (pBits) free(pBits); CloseHandle(hFile); return 0; } // Read in the Information Header bSuccess = ReadFile(hFile, pbmi, iInfoSize, &dwBytesRead, NULL); if (!bSuccess || (int)dwBytesRead != iInfoSize) { MessageBox(hwnd, TEXT("Cannot read file."), szAppName, MB_ICONWARNING | MB_OK); if (pbmi) free(pbmi); if (pBits) free(pBits); CloseHandle(hFile); return 0; } // Get the DIB width and height bTopDown = FALSE; if (pbmi->bmiHeader.biSize == sizeof(BITMAPCOREHEADER)) { cxDib = ((BITMAPCOREHEADER *)pbmi)->bcWidth; cyDib = ((BITMAPCOREHEADER *)pbmi)->bcHeight; cBits = ((BITMAPCOREHEADER *)pbmi)->bcBitCount; } else { if (pbmi->bmiHeader.biHeight < 0) bTopDown = TRUE; cxDib = pbmi->bmiHeader.biWidth; cyDib = abs(pbmi->bmiHeader.biHeight); cBits = pbmi->bmiHeader.biBitCount; if (pbmi->bmiHeader.biCompression != BI_RGB && pbmi->bmiHeader.biCompression != BI_BITFIELDS) { MessageBox(hwnd, TEXT("File is compressed."), szAppName, MB_ICONWARNING | MB_OK); if (pbmi) free(pbmi); if (pBits) free(pBits); CloseHandle(hFile); return 0; } } // Get the row length iRowLength = ((cxDib * cBits + 31) & ~31) >> 3; // Read and display SetCursor(LoadCursor(NULL, IDC_WAIT)); ShowCursor(TRUE); hdc = GetDC(hwnd); for (y = 0; y < cyDib; ++y) { ReadFile(hFile, pBits + y * iRowLength, iRowLength, &dwBytesRead, NULL); SetDIBitsToDevice(hdc, 0, // xDst 0, // yDst cxDib, // cxSrc cyDib, // cySrc 0, // xSrc 0, // ySrc bTopDown ? cyDib - y - 1 : y, // first scan line 1, // number of scan lines pBits + y * iRowLength, pbmi, DIB_RGB_COLORS); } ReleaseDC(hwnd, hdc); CloseHandle(hFile); ShowCursor(FALSE); SetCursor(LoadCursor(NULL, IDC_ARROW)); return 0; } break; case WM_PAINT: hdc = BeginPaint(hwnd, &ps); if (pbmi && pBits) SetDIBitsToDevice(hdc, 0, // xDst 0, // yDst cxDib, // cxSrc cyDib, // cySrc 0, // xSrc 0, // ySrc 0, // first scan line cyDib, // number of scan lines pBits, pbmi, DIB_RGB_COLORS); EndPaint(hwnd, &ps); return 0; case WM_DESTROY: if (pbmi) free(pbmi); if (pBits) free(pBits); PostQuitMessage(0); return 0; } return DefWindowProc(hwnd, message, wParam, lParam); }
SEQDISP.RC (excerpts) // Microsoft Visual C++ generated resource script. // #include "resource.h" ///////////////////////////////////////////////////////////////////////////// // // Menu // SEQDISP MENU BEGIN POPUP "&File" BEGIN MENUITEM "Open...\tCtrl+O", IDM_FILE_OPEN END END ///////////////////////////////////////////////////////////////////////////// // // Accelerator // SEQDISP ACCELERATORS BEGIN "O", IDM_FILE_OPEN, VIRTKEY, CONTROL, NOINVERT END
RESOURCE.H (excerpts) // Microsoft Visual C++ 生成的包含文件。 // 供 SeqDisp.rc 使用 // #define IDM_FILE_OPEN 40001
SEQDISP.C 程序里的所有文件 I/O 操作都发送在处理文件打开命令时。在 WM_COMMAND 消息处理的最后,程序进入一个循环,此循环读入每个像素行,然后用 SetDIBitsToDevice 显示这些像素行。整个 DIB 被保留在内存中,因此在 WM_PAINT 消息处理中也能显示它。
15.2.5 拉伸到合适大小
SetDIBitsToDevice 把 DIB 按一一对应的像素的方式显示到输出设备。但这可能不适合打印 DIB。打印机的分辨率越好,你得到的图像就越小。有可能你最后会得到一张邮票大小的图像。通过缩小或拉伸一个 DIB,可以在输出设备上按特定大小显示它,为此你可以使用 StretchDIBits:
iLines = StretchDIBits ( hdc, // device context handle xDst, // x destination coordinate yDst, // y destination coordiante cxDst, // destination rectangle width cyDst, // destination rectangle height xSrc, // x source coordiante ySrc, // y source coordinate cxSrc, // source rectangle width cySrc, // source rectangle height pBits, // pointer to DIB pixel bits pInfo, // pointer to DIB information fClrUse, // color use flag dwRop); // raster operation
该函数的参数和 SetDIBitsToDevice 一样,但有三点例外:
目标坐标包含逻辑宽度(cxDst)、逻辑高度(cyDst)和起始点。
此函数并不能通过顺序显示 DIB 来减少内存需求。
最后一个参数是光栅操作,它指出了 DIB 的像素怎样和输出设备的像素结合起来。在第 14 章里我们学习过这些光栅操作。目前我们把这个参数设置成 SRCCOPY。
这里还有一个更微妙的区别。如果你看一下 SetDIBitsToDevice 的定义,就会发现 cxSrc 和 cySrc 是 DWORD 类型——32 位无符号长整数类型。而 StretchDIBits 里的 cxSrc 和 cySrc(以及 cxDst 和 cyDst)则被定义成有符号整数,即它们的值可以是负数。事实上它们的值也确实是可以取负数的,很快我们就会看到。如果你为此开始检查其他的参数是否可以是负数,那么我来澄清一点:在这两个函数里,xSrc
和 ySrc 被定义成 int 值,但是这其实是错的。这些值总是非负数。
下表给出了 DIB 里的源矩形到目标矩形的映射。
源 矩 形 | 目标矩形 |
---|---|
(xSrc, ySrc) | (xDst, yDst + cyDst - 1) |
(xSrc + cxSrc - 1, ySrc) | (xDst + cxDst - 1, yDst + cyDst - 1) |
(xSrc, y + cySrc - 1) | (xDst, yDst) |
(xSrc + cxSrc - 1, y + cySrc - 1) | (xDst + cxDst - 1, yDst) |
举个例子,假设有个 2 * 2 的 DIB,StretchDIBits 的 xSrc 和 ySrc 参数值都是 0,cxSrc 和 cySrc 的值都是 2。假设我们要用 MM_TEXT 映射模式把 DIB 显示到设备环境,并且不用任何变换。如果 xDst 和 yDst 都是 0,并且 cxDst 和 cyDst 都是 4,那么我们就是以两倍大小来拉伸 DIB。每个源像素(x, y)将会映射到四个目标像素,具体如下所示:
(0,0) --> (0,2) and (1,2) and (0,3) and (1,3) (1,0) --> (2,2) and (3,2) and (2,3) and (3,3) (0,1) --> (0,0) and (1,0) and (0,1) and (1,1) (1,1) --> (2,0) and (3,0) and (2,1) and (3,1)
上表正确给出了目标矩形的四个角,即(0, 3),(3, 3),(0, 0)和(3, 0)。在其他情况下,坐标可能仅仅是近似的值。
仅当 xDst 和 yDst 是逻辑坐标时,SetDIBitsToDevice 才会受目标设备环境的映射模式的影响。而 StretchDIBits 则完全受映射模式的影响。例如,如果将度量映射模式设置为 y 值随着向上显示而增加,那么 DIB 就会被上下颠倒显示。
要想避免类似问题,可以把 cyDst 设成负数。事实上,可以把任何宽度和高度参数设成负数,使得 DIB 沿水平方向或垂直方向翻转。在 MM_TEXT 映射模式下,如果 cySrc 和 cyDst 的符号相反,则 DIB 会沿水平轴翻转,看起来就是上下颠倒的。如果 cxSrc 和cxDst 的符号相反,则 DIB 会沿垂直轴翻转,看起来就是一个镜像。
我们可以用两个表达式来对此进行总结。在这两个表达式中,xMM 和 yMM 给出了映射模式的取向:如果 x 随着从左到右显示而增加,xMM 就是 1;如果 x 随着从右到左显示而增加,xMM 是 -1;同样,如果 y 随着从上到下显示而增加,yMM 就是 1;如果 y 随着从下到上显示而增加,yMM 就是 -1。Sign 函数对正数返回 TRUE,对负数返回 FALSE:
if (!Sign(xMM * cxSrc * cxDst)) DIB 沿垂直轴翻转 (镜像) if (!Sign(yMM * cySrc * cyDst)) DIB 沿水平轴翻转 (上下颠倒)如果有疑问,请参加前面的表。
下面所示的 SHOWDIB2 程序按 DIB 的实际大小显示它,并且把它拉伸到客户区窗口的大小,接着打印 DIB,并把 DIB 传输到剪贴板。
/*------------------------------------------------- SHOWDIB2.C -- Shows a DIB in the client area (c) Charles Petzold, 1998 --------------------------------------------------*/ #include <windows.h> #include "dibfile.h" #include "resource.h" LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); TCHAR szAppName[] = TEXT("ShowDib2"); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevinstance, PSTR szCmdLine, int iCmdShow) { HACCEL hAccel; HWND hwnd; MSG msg; WNDCLASS wndclass; wndclass.style = CS_HREDRAW | CS_VREDRAW; wndclass.lpfnWndProc = WndProc; wndclass.cbClsExtra = 0; wndclass.cbWndExtra = 0; wndclass.hInstance = hInstance; wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION); wndclass.hCursor = LoadCursor(NULL, IDC_ARROW); wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); wndclass.lpszMenuName = szAppName; wndclass.lpszClassName = szAppName; if (!RegisterClass(&wndclass)) { MessageBox(NULL, TEXT("This program requires Windows NT!"), szAppName, MB_ICONERROR); return 0; } hwnd = CreateWindow(szAppName, TEXT("Show DIB #2"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL); ShowWindow(hwnd, iCmdShow); UpdateWindow(hwnd); hAccel = LoadAccelerators(hInstance, szAppName); while (GetMessage(&msg, NULL, 0, 0)) { if (!TranslateAccelerator(hwnd, hAccel, &msg)) { TranslateMessage(&msg); DispatchMessage(&msg); } } return msg.wParam; } int ShowDib(HDC hdc, BITMAPINFO * pbmi, BYTE * pBits, int cxDib, int cyDib, int cxClient, int cyClient, WORD wShow) { switch (wShow) { case IDM_SHOW_NORMAL: return SetDIBitsToDevice(hdc, 0, 0, cxDib, cyDib, 0, 0, 0, cyDib, pBits, pbmi, DIB_RGB_COLORS); case IDM_SHOW_CENTER: return SetDIBitsToDevice(hdc, (cxClient - cxDib) / 2, (cyClient - cyDib) / 2, cxDib, cyDib, 0, 0, 0, cyDib, pBits, pbmi, DIB_RGB_COLORS); case IDM_SHOW_STRETCH: SetStretchBltMode(hdc, COLORONCOLOR); return StretchDIBits(hdc, 0, 0, cxClient, cyClient, 0, 0, cxDib, cyDib, pBits, pbmi, DIB_RGB_COLORS, SRCCOPY); case IDM_SHOW_ISOSTRETCH: SetStretchBltMode(hdc, COLORONCOLOR); SetMapMode(hdc, MM_ISOTROPIC); SetWindowExtEx(hdc, cxDib, cyDib, NULL); SetViewportExtEx(hdc, cxClient, cyClient, NULL); SetWindowOrgEx(hdc, cxDib / 2, cyDib / 2, NULL); SetViewportOrgEx(hdc, cxClient / 2, cyClient / 2, NULL); return StretchDIBits(hdc, 0, 0, cxDib, cyDib, 0, 0, cxDib, cyDib, pBits, pbmi, DIB_RGB_COLORS, SRCCOPY); } } LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static BITMAPFILEHEADER * pbmfh; static BITMAPINFO * pbmi; static BYTE * pBits; static DOCINFO di = { sizeof(DOCINFO), TEXT("ShowDib2: Printing") }; static int cxClient, cyClient, cxDib, cyDib; static PRINTDLG printdlg = { sizeof(PRINTDLG) }; static TCHAR szFileName[MAX_PATH], szTitleName[MAX_PATH]; static WORD wShow = IDM_SHOW_NORMAL; BOOL bSuccess; HDC hdc, hdcPrn; HGLOBAL hGlobal; HMENU hMenu; int cxPage, cyPage, iEnable; PAINTSTRUCT ps; BYTE * pGlobal; switch (message) { case WM_CREATE: DibFileInitialize(hwnd); return 0; case WM_SIZE: cxClient = LOWORD(lParam); cyClient = HIWORD(lParam); return 0; case WM_INITMENUPOPUP: hMenu = GetMenu(hwnd); if (pbmfh) iEnable = MF_ENABLED; else iEnable = MF_GRAYED; EnableMenuItem(hMenu, IDM_FILE_SAVE, iEnable); EnableMenuItem(hMenu, IDM_FILE_PRINT, iEnable); EnableMenuItem(hMenu, IDM_EDIT_CUT, iEnable); EnableMenuItem(hMenu, IDM_EDIT_COPY, iEnable); EnableMenuItem(hMenu, IDM_EDIT_DELETE, iEnable); return 0; case WM_COMMAND: hMenu = GetMenu(hwnd); switch (LOWORD(wParam)) { case IDM_FILE_OPEN: // Show the File Open dialog box if (!DibFileOpenDlg(hwnd, szFileName, szTitleName)) return 0; // If there's an existing DIB, free the memory if (pbmfh) { free(pbmfh); pbmfh = NULL; } // Load the entire DIB into memory SetCursor(LoadCursor(NULL, IDC_WAIT)); ShowCursor(TRUE); pbmfh = DibLoadImage(szFileName); ShowCursor(FALSE); SetCursor(LoadCursor(NULL, IDC_ARROW)); // Invalidate the client area for later update InvalidateRect(hwnd, NULL, TRUE); if (pbmfh == NULL) { MessageBox(hwnd, TEXT("Cannot load DIB file"), szAppName, 0); return 0; } // Get pointers to the info structure & the bits pbmi = (BITMAPINFO *)(pbmfh + 1); pBits = (BYTE *)pbmfh + pbmfh->bfOffBits; // Get the DIB width and height if (pbmi->bmiHeader.biSize == sizeof(BITMAPCOREHEADER)) { cxDib = ((BITMAPCOREHEADER *)pbmi)->bcWidth; cyDib = ((BITMAPCOREHEADER *)pbmi)->bcHeight; } else { cxDib = pbmi->bmiHeader.biWidth; cyDib = abs(pbmi->bmiHeader.biHeight); } return 0; case IDM_FILE_SAVE: // Show the File Save dialog box if (!DibFileSaveDlg(hwnd, szFileName, szTitleName)) return 0; // Save the DIB to memory SetCursor(LoadCursor(NULL, IDC_WAIT)); ShowCursor(TRUE); bSuccess = DibSaveImage(szFileName, pbmfh); ShowCursor(FALSE); SetCursor(LoadCursor(NULL, IDC_ARROW)); if (!bSuccess) MessageBox(hwnd, TEXT("Cannot save DIB file"), szAppName, 0); return 0; case IDM_FILE_PRINT: if (!pbmfh) return 0; // Get printer DC printdlg.Flags = PD_RETURNDC | PD_NOPAGENUMS | PD_NOSELECTION; if (!PrintDlg(&printdlg)) return 0; if (NULL == (hdcPrn = printdlg.hDC)) { MessageBox(hwnd, TEXT("Cannot obtain Printer DC"), szAppName, MB_ICONEXCLAMATION | MB_OK); return 0; } // Check whether the printer can print bitmaps if (!(RC_BITBLT & GetDeviceCaps(hdcPrn, RASTERCAPS))) { DeleteDC(hdcPrn); MessageBox(hwnd, TEXT("Printer cannot print bitmaps"), szAppName, MB_ICONEXCLAMATION | MB_OK); return 0; } // Get size of printable area of page cxPage = GetDeviceCaps(hdcPrn, HORZRES); cyPage = GetDeviceCaps(hdcPrn, VERTRES); bSuccess = FALSE; // Send the DIB to the printer SetCursor(LoadCursor(NULL, IDC_WAIT)); ShowCursor(TRUE); if ((StartDoc(hdcPrn, &di) > 0) && (StartPage(hdcPrn) > 0)) { ShowDib(hdcPrn, pbmi, pBits, cxDib, cyDib, cxPage, cyPage, wShow); if (EndPage(hdcPrn) > 0) { bSuccess = TRUE; EndDoc(hdcPrn); } } ShowCursor(FALSE); SetCursor(LoadCursor(NULL, IDC_ARROW)); DeleteDC(hdcPrn); if (!bSuccess) MessageBox(hwnd, TEXT("Could not print bitmap"), szAppName, MB_ICONEXCLAMATION | MB_OK); return 0; case IDM_EDIT_COPY: case IDM_EDIT_CUT: if (!pbmfh) return 0; // Make a copy of the packed DIB hGlobal = GlobalAlloc(GHND | GMEM_SHARE, pbmfh->bfSize - sizeof(BITMAPFILEHEADER)); pGlobal = (BYTE *)GlobalLock(hGlobal); CopyMemory(pGlobal, (BYTE *)pbmfh + sizeof(BITMAPFILEHEADER), pbmfh->bfSize - sizeof(BITMAPFILEHEADER)); GlobalUnlock(hGlobal); // Transfer it to the clipboard OpenClipboard(hwnd); EmptyClipboard(); SetClipboardData(CF_DIB, hGlobal); CloseClipboard(); if (LOWORD(wParam) == IDM_EDIT_COPY) return 0; // fall through if IDM_EDIT_CUT case IDM_EDIT_DELETE: if (pbmfh) { free(pbmfh); pbmfh = NULL; InvalidateRect(hwnd, NULL, TRUE); } return 0; case IDM_SHOW_NORMAL: case IDM_SHOW_CENTER: case IDM_SHOW_STRETCH: case IDM_SHOW_ISOSTRETCH: CheckMenuItem(hMenu, wShow, MF_UNCHECKED); wShow = LOWORD(wParam); CheckMenuItem(hMenu, wShow, MF_CHECKED); InvalidateRect(hwnd, NULL, TRUE); return 0; } break; case WM_PAINT: hdc = BeginPaint(hwnd, &ps); if (pbmfh) ShowDib(hdc, pbmi, pBits, cxDib, cyDib, cxClient, cyClient, wShow); EndPaint(hwnd, &ps); return 0; case WM_DESTROY: if (pbmfh) free(pbmfh); PostQuitMessage(0); return 0; } return DefWindowProc(hwnd, message, wParam, lParam); }
SHOWDIB2.RC (excerpts) // Microsoft Visual C++ generated resource script. // #include "resource.h" ///////////////////////////////////////////////////////////////////////////// // // Menu // SHOWDIB2 MENU BEGIN POPUP "&File" BEGIN MENUITEM "&Open...\tCtrl+O", IDM_FILE_OPEN MENUITEM "&Save...\tCtrl+S", IDM_FILE_SAVE MENUITEM SEPARATOR MENUITEM "&Print\tCtrl+P", IDM_FILE_PRINT END POPUP "&Edit" BEGIN MENUITEM "Cu&t\tCtrl+X", IDM_EDIT_CUT MENUITEM "&Copy\tCtrl+C", IDM_EDIT_COPY MENUITEM "&Delete\tDelete", IDM_EDIT_DELETE END POPUP "&Show" BEGIN MENUITEM "&Actual Size", IDM_SHOW_NORMAL, CHECKED MENUITEM "&Center", IDM_SHOW_CENTER MENUITEM "&Stretch to Window", IDM_SHOW_STRETCH MENUITEM "Stretch &Isotropically", IDM_SHOW_ISOSTRETCH END END ///////////////////////////////////////////////////////////////////////////// // // Accelerator // SHOWDIB2 ACCELERATORS BEGIN "C", IDM_EDIT_COPY, VIRTKEY, CONTROL, NOINVERT "O", IDM_FILE_OPEN, VIRTKEY, CONTROL, NOINVERT "P", IDM_FILE_PRINT, VIRTKEY, CONTROL, NOINVERT "S", IDM_FILE_SAVE, VIRTKEY, CONTROL, NOINVERT VK_DELETE, IDM_EDIT_DELETE, VIRTKEY, NOINVERT "X", IDM_EDIT_CUT, VIRTKEY, CONTROL, NOINVERT END
RESOURCE.H (excerpts) // Microsoft Visual C++ 生成的包含文件。 // 供 ShowDib2.rc 使用 // #define IDM_FILE_OPEN 40001 #define IDM_FILE_SAVE 40002 #define IDM_FILE_PRINT 40003 #define IDM_EDIT_CUT 40004 #define IDM_EDIT_COPY 40005 #define IDM_EDIT_DELETE 40006 #define IDM_SHOW_NORMAL 40007 #define IDM_SHOW_CENTER 40008 #define IDM_SHOW_STRETCH 40009 #define IDM_SHOW_ISOSTRETCH 40010
这里值得注意的是 ShowDib 函数,它根据菜单选项,按四种不同的方式在程序的客户区显示 DIB。SetDIBitsToDevice 函数可以把 DIB 显示在客户区的左上角,或者也可以把它显示在客户区的中间。使用 StretchDIBits 函数,也可以安两种方式显示 DIB:DIB 可以被拉伸到填满客户区,在这种情况下 DIB 可能会变形;或者各个方向都按相同比例拉伸,也就是说,DIB 不会变形。
把 DIB 复制到剪贴板的过程包括在全局共享内存中复制一份紧凑格式的 DIB 内存块。相应的剪贴板数据类型是 CF_DIB。程序没有给出怎样从剪贴板中复制 DIB。这是因为在只有一个指向紧凑 DIB 内存块的指针的情况下,要确定像素位的位移需要有更多的处理逻辑。在第 16 章结束前我将会告诉你该怎样做。
你可能还注意到了 SHOWDIB2 里的其他一些不足。如果在 256 色视频模式下运行 Windows,除了单色或 4 位 DIB,在显示其他 DIB 时你将会发现问题。颜色不能被正确显示。访问那些颜色需要使用调色板,这将在第 16 章介绍。你可能也会注意到速度的问题,尤其是在 Windows NT 下运行 SHOWDIB2 时。在第 16 章介绍 DIB 和位图时,我会介绍怎样处理这些问题。而且我还会在显示
DIB 时加入滚动条,如此一来,我们就可以查看实际大小比屏幕大的 DIB。
15.2.6 颜色转换、调色板和性能
还记得在 William Goldman 的电影 All the President's Men(《总统班底》,又译作《惊天大阴谋》和《水门事件》)中,Deep Throat 告诉 WoodWard,揭开水门事件神秘之处的关键是“跟着钱走”吗?同样,显示位图时要想获得高性能,关键就在于“跟着像素位走”,并且理解颜色转换在何时何处发生。DIB 是一种设备无关的格式;视频显示内存采用的则肯定是另一种格式。在SetDIBitsToDevice 和 StretchDIBits 函数调用过程中,必须把每个像素(可能有几百万个)从设备无关格式转换成设备相关格式。
在许多情况,这个转换工作量并不大。例如,如果想在一个 24 位视频显示器上显示一个 24 位的 DIB,显示驱动程序最多只需要交换红、绿、蓝字节的顺序。在一个 24 位设备上显示一个 16 位的 DIB,则需要移动和填充一些像素位。在一个 16 位设备上显示一个 24 位的 DIB,需要移动和截断一些像素位。在一个
24 位设备上显示一个 4 位或 8 位的 DIB,需要在 DIB 的颜色表里查找 DIB 像素位,然后有可能需要把一些字节重新排列。
但如果想在一个 4 位或者 8 位的视频显示器上显示 16 位、24 位甚至是 32 位的 DIB,会发生什么情况呢?与之前不同,这需要彻底的颜色转换。设备驱动程序得在像素和所有能显示出来的颜色中,为 DIB 中的每个像素寻找一个最接近的颜色。这个过程涉及循环和计算。(GDI 函数 GetNearestColor 可以搜索最相近的颜色。)
整个三维 RGB 颜色矩阵可以用一个立方体来表示。而该曲面中任意两点的距离为:
其中,两种颜色分别是 R1G1B1 和 R2G2B2。搜索最相近的颜色涉及寻找从一种颜色到其他颜色集合的最短路径。幸运的是,在比较 RGB 颜色立方体中的距离时,不需要计算平方根。但是我们必须把每个将进行颜色转换的像素和设备的所有颜色进行比较,以便找出最接近像素的设备颜色。这个工作量仍然不小。(尽管在一个 8 位的设备上显示 8 位的 DIB 也设计最近颜色搜索,但这个操作不是对每个像素都必须做的;你仅仅需要对
DIB 颜色表中的每种颜色做最相近颜色搜索。)
因此,我们应该避免用 SetDIBitsToDevice 或 StretchDIBits 函数在 8 位视频显示适配器上显示 16 位、24 位或 32 位 DIB。这些 DIB 应该被转换成 8 位 DIB,或者要想性能更好的话,转换成 8 位 DDB。事实上,通过把 DIB 转换成 DDB,再使用 BitBlt 和 StretchBlt 把图像显示出来,我们可以加快几乎所有大小的 DIB 显示过程。
如果在一个 8 位视频显示器上运行 Windows(或者你因为想知道显示全彩 DIB 时的性能差异而刚刚切换到 8 位模式),你会发现另一个问题:并不是 DIB 的所有颜色都能被显示出来。任何在 8 位视频显示器上显示的 DIB 被限定成只有 20 种颜色。要想使用 20 种以上的颜色,你需要用到调色板管理器,详情请参见第
16 章。
最后,如果在一台机器上同时运行 Windows 98 和 Windows NT,你可能会注意到在用类似的视频模式显示很大的 DIB 时,Windows NT 需要的时间更长。这是 Windows NT 的客户、服务器体系结构造成的,在这种体系结构下,通过 API 传输大量数据需要付出速度的代价。解决方案仍然是把 DIB 转换成 DDB。此外,后面即将介绍的 CreateDIBSection 函数也是针对这种情况专门设计的。
相关文章推荐
- (NO.00004)iOS实现打砖块游戏(六):反弹棒类
- (NO.00004)iOS实现打砖块游戏(六):反弹棒类
- (NO.00004)iOS实现打砖块游戏(六):反弹棒类
- [LeetCode]Bulls and Cows
- Struts2中使用servletresponse直接输出内容到客户端出现:Cannot call sendError() after the response has been committed
- 百度地图的使用(1)
- 网络编程基础API
- Oracle memory troubleshooting, Part 1: Heapdump Analyzer
- linux下ftp常用命令
- iOS MRC手动内存管理 心得体会
- (NO.00004)iOS实现打砖块游戏(五):游戏场景类
- poj 2195 - Going Home 二分图最优匹配 ek
- 专业英语之路——基本口语阶段(一)
- (NO.00004)iOS实现打砖块游戏(五):游戏场景类
- (NO.00004)iOS实现打砖块游戏(五):游戏场景类
- 多重对数函数
- Construct Binary Tree from Inorder and Postorder Traversal
- 一起talk C栗子吧(第六十七回:C语言实例--DIY字符串长度函数)
- 【UML之用例图】
- iOS 引入库工程遇到的问题