图像处理之-位图(未完待續)

图像处理之-位图

  • MD DoCumEnT: 3/16/2016 5:59:48 PM by Jimbowhy

自从发现MarkdownPad以后,就沉迷于写作,从未有过这样的浸淫,完全没有了生物钟的同期,基本上只要醒着,手眼就离不了屏幕,离不了键盘,一直敲着几近光滑的按键,那种感觉就是满足,如果要用个词来形容,我觉得 F**KING WRITING! F**KING MY LIFE! 是恰当的。生命有终结的一天,而文字却不会。 - by Jimbowhy 3/20/2016 4:32:04 PM

虽然本文的标题只说是位图图像处理,其实内容远比标题丰富,原本计划是只涉及位图的文件结构分析和代码实现。但一头扎下去,就搞大了,从GDI到CONSOLE,从VGA到API绘图,我觉得比较有趣的点基本都染指了。因为图像处理本来就是很深又广的领域,程序开发过程中不免也要和图形打交道,而且BMP对于当前使用的Window操作系统是如此的重要,以致为它写一本书的内容都是可以收集到的。

背景知识

位图,BMP文件是 Windows、OS/2 操作系统的常用图形文件格式,这里虽然用“常用”来形容BMP,其实把它称为 Windows 基石也不为过,Windows 整个 GDI 系统都是围绕它进行的。Win32程序的窗体都是通过位图绘制出来的,可以说,没有位图就没有Windows。它是设备无关的 DIB device-independent bitmaps,当然系统中常用的还有设备相关的位图 DDB device-dependnt bitmaps。支持多种色深,色深用 BPP Bits-Per-Pixel 表示,有 1bpp 2bpp 4bpp 8bpp 16bpp 24bpp 32bpp 多种,可以保存使用色板的黑白双色图 monochrome,4色,16色,256色图,对于其它更高的色深图片则直接保存色值到像素所在的位置。对于低色深的位图则在像素数据中保存色板颜色的索引号。为此,对于一个2色位置而言,每个像素只点一个比特,一个字节就可以保存8个像素了。至于DDB和DIB有什么具体差别,可以从后面的函数操作过程中理解,在这里可以将DDB理解为只有二维数据的位图。另外,从BMP文件的信息头也可以形像理解DIB的特点:

  • 有独立的颜色信息;
  • 有位图创建时的设备尺寸信息;
  • 有位图创建时的设备色板信息;
  • 有RGB三分量的色目数组数据映射到像素上;
  • 有数据的压缩方式相关信息;

因此DIB位图文件格式可以保存二维 two-dimensional 数字图像,文件中包含了图像宽度,高度,分辨率,色深信息,还有可选的压缩方法,alpha通道,颜色配置信息,在 Windows Metafile (WMF) specification 中对位置文件格式有详细说明,在C语言头文件 wingdi.h 中定义了和位图相关的常量、结构体。典型的BMP文件至少包含三个部分,BMP文件头、DIB信息头和像素数据,到于低色深位图还有调色板 Palette,所谓调色板就是一个数组,每个元素使用四个字节定义一个RGB色值。调色板紧接DIB信息头存储。现在常用的BMP格式是 BMP Version 3,这个格式版本从 Windows 3.x 就开始使用了。以下是我重写的结构体定义:

#pragma pack(push,2)
typedef struct BitmapHeader {   // BITMAPFILEHEADER
    unsigned short  bfType;     // 0x4d42; it occupy 4bytes if memory aligned
    unsigned int    bfSize;     // DWORD bitmap file size
    unsigned short  bfReserved1;
    unsigned short  bfReserved2;
    unsigned int    bfOffBits;  // offset to the bitmap bits data
} BitmapHeader;
#pragma pack(pop)

typedef struct BitmapInfo{      // BITMAPINFOHEADER
    unsigned int    biSize;     // DWORD the size of this structure.
            long    biWidth;    // LONG the width of the bitmap, in pixels. 
            long    biHeight; 
    unsigned short  biPlanes;   // WORD always is 1!
    unsigned short  biBitCount; // the number of bits-per-pixel. 1 for monochrome bmiColors
    unsigned int    biCompression; // BI_RGB(uncompressed),BI_RLE8,BI_RLE4 ...
    unsigned int    biSizeImage;   // the size, in bytes, of the image. may be zero for BI_RGB.
            long    biXPelsPerMeter; 
            long    biYPelsPerMeter; 
    unsigned int    biClrUsed; 
    unsigned int    biClrImportant; 
} BitmapInfo, *PBitmapInfo; 

一般Win32平台的位图总是 0x42 0x4D 两个字节开头的,即 BM 两个字符,当然 bfType 有可能是以下的任意一种:

BM – Windows 3.1x, 95, NT, ... etc.
BA – OS/2 struct bitmap array
CI – OS/2 struct color icon
CP – OS/2 const color pointer
IC – OS/2 struct icon
PT – OS/2 pointer

BMP文件大小就保存在 bfSize 中,其实这个有点多余,通过文件读取就可以得到BMP文件的大小了。然后就是 bfOffBits,它指出了BMP文件像素数据到文件开始字节的偏移量,结合BMP文件头和DIB信息头就可以计算到调色板的数据起止点。注意定义BMP文件头 BitmapHeader 时,因为它第一个成员是2个字节的,如果编译有对齐,那么文件头结构体就会变成16个字节,这就不对了,因此需要设置编译器对齐属性。

biCompression 是压缩信息,一般情况用得最多的是无压缩格式 BI_RGB,可选值如下。但是只有自底向上 Bottom-up 的位图才可以压缩, Top-down DIB 不可压缩。那么 Top-Down vs. Bottom-Up DIBs,什么是自底向上呢?所谓自底向上是指图片的像素在内存存储的顺序是先保存图片的最底下一行,再上一行这样进行的。对 Bottom-up 的位图,内存的第一个字节是保存图片的左下角那个像素的。在GDI中所有的DIB都是 Bottom-up 方式处理的。

BI_RGB      An uncompressed format. 
BI_RLE8     A run-length encoded (RLE) format for bitmaps with 8 bpp. 
            Consisting of a count byte followed by a byte containing a color index. 
BI_RLE4     An 2-byte RLE format for bitmaps with 4 bpp. 
            Consisting of a count byte followed by two word-length color indexes. 
BI_BITFIELDS Specifies that the color table consists of three DWORD color masks for 16/32-bpp.
BI_JPEG     Windows 98, Windows 2000: Indicates that the image is a JPEG image. 
BI_PNG      Windows 98, Windows 2000: Indicates that the image is a PNG image. 

在BMP的压缩方法基本都是游程码方式,这是一种算法简单的压缩方式。如果说1亿这个数,它在1的后面跟了8个0,那游程码可以表示为1180,这种通过一个值来表示被压缩内容长度的方法就是游程编程 RLE Run-Length Encode。前面讲到位图可以有16-bit/24-bit/32-bit几种,如果中间这种,那么每个像素用3个字节表示,刚好每个颜色分量占一个字节。但是对于另外两种,情况就不同了。16-bit的色深,每个颜色分量可以占5~6比特,这就涉及怎么安排RGB各颜色分量的位宽,Windows 95 只支持 RGB555 和 RGB565,还有32bpp模式的 RGB888。对于24bpp,还可以通过 DIB信息头的 biClrUsed 来指定索引色数量,这样在位图使用的色彩数目较少的情况下来优化系统调色板,不过这种方法使用极少。当 biClrUsed 的数值为 0 时表示索引色为指定色深的最大索引数量。

为了使用不同的RGB分量位宽,需要指定压缩信息为 BI_BITFIELDS,这里,调色板保存的就不是颜色定义,而是分量分割掩码。有三个掩码对应RGB三个分量,每个掩码为32-bit,举例来说 RGB555、RGB888两个模式下每个分量的位宽是分别是5-bit、8-bit,掩码定义如下:

The RGB555 format masks would look like: 
0x00007C00  red   (0000 0000 0000 0000 0111 1100 0000 0000)
0x000003E0  green (0000 0000 0000 0000 0000 0011 1110 0000)
0x0000001F  blue  (0000 0000 0000 0000 0000 0000 0001 1111)

The RGB888 format masks would look like: 
0x00FF0000  red   (0000 0000 1111 1111 0000 0000 0000 0000)
0x0000FF00  green (0000 0000 0000 0000 1111 1111 0000 0000)
0x000000FF  blue  (0000 0000 0000 0000 0000 0000 1111 1111)

在WIKI上有演示掩码的定义格式:

BITFIELDS mechanism
Diag. 2 – The BITFIELDS mechanism for a 32-bit pixel depicted in RGBAX sample length notation

DIB数据处理与应用

有了上面的数据结构,就可以通过 CreateDIBitmap() 函数来构造位图了,注意这个函数创建的是DDB位图,虽然名称为CreateDIBitmap,这确实会让人误解,但它是通过DIB数据来创建DDB位图的函数。给它设置参数 CBM_INIT 时,它就会使用色板和像素数据来初始化新建的DDB位图,因此这里就会有DIB数据到DDB数据对拷。这个过程等价于使用 CreateCompatibleBitmap() 函数来创建DDB位图,然后再使用 SetDIBits() 来向DDB拷贝DIB位图数据,同样这个函数名也有点容易误解,它应该理解为设置DIB数据到位图中。这里提到的两个函数都是用来创建DDB的,所以第一个参数传入的DC其实就是创建位图所依赖的设备,特别是调色板。来看看MSDN对 CreateDIBitmap() 这个函数的最后一个参数 fuUsage 的解析:

Specifies whether the bmiColors member of the BITMAPINFO structure was initialized and, if so, whether bmiColors contains explicit red, green, blue (RGB) values or palette indexes. The fuUsage parameter must be one of the following values.

在不理解DIB和DDB的区别前,理解这段话是有难度,因为会不知所云。回到文章的开头,DIB和DDB的最大的区别就是色值信息的保存,很有意思的。DDB可以理解为色值和像素数据是一体的,像DC中所使用位图就是。而DIB则不是了,它可以将颜色保存在调色板,也可以在位图像素中保存,如16bbp、24bbp、32bbp等等色深的位图文件就是。因此这个参数的作用就是通过指定 DIB_PAL_COLORS 来使用输入参数DC上的调色板,指定 DIB_RGB_COLORS 来使用像素数据的色值。系统中只有唯一一个结构体是描述DDB的:

typedef struct tagBITMAP {
  LONG   bmType; 
  LONG   bmWidth; 
  LONG   bmHeight; 
  LONG   bmWidthBytes; 
  WORD   bmPlanes; 
  WORD   bmBitsPixel; 
  LPVOID bmBits; 
} BITMAP, *PBITMAP; 

CreateDIBSection()才是真正创建DIB位图的函数,其实bmp文件就是DIB位图,所以通过文件流读入的位图文件二进位数据就可以用在这个函数中。在传入参数 BITMAPINFO 结构体就是数据入口,这个结构体不包含了 bmp 文件头和像素数据,只含有DIB信息头和调色板两部分数据。所以只要将读取的位图文件的开头偏移一个14个字节,即一个bmp文件头的长度后的数据传入,并设置好偏移参数 dwOffset 即可。在输出参数 ppvBits 就会指向包含DIB像素数据的内存地址。注意,输入位图的像素数据是通过 hSection 参数传入的。通过这个函数的学习,其实可以将位图文件的像素数据理解为 Section 更合适,这样可以和MSDN文档相统一,而且像素这概念通过用来表述显示器上看得到的点,是具有颜色特征的。而BMP文件中的像素数据其实并不一定就是一个色值,还可以是色板的索引号码。在使用这个函数时,需要传入一个DC,当指定参数 DIB_PAL_COLORS 时,函数就会使用DC上的调色板来初始化像素。指定 DIB_RGB_COLORS 时则使用 bmiColors 的色板信息。

到这里可以理解DIB和DDB的另一个重要的区别,DDB可以在DC关联的设备上显示,而DIB则需要经过调色板的映射转换,这就是DDB实用时效率更高,而DIB在各种设备之间转换时兼容性更好。在转换的过程中注涉及了位图中的逻辑色板和设备上的物理色板,注意这里指的是硬件上设备,每种硬件可显示的色彩都是有范围的,这个色彩显示能力就是物理色板的抽象概念。下面这幅图可以帮助理解DDB是怎样提高显示效率的:

+----------------------------------------+----------------------------------------+
|            Client Side                 |                Server Side             |
|                               +--------+--------+                               |
|                               |      Event      |                               |
|                               +--------+--------+                               |
|                           +------------+-----------+                            |
|                           |     Memory Windows     |                            |
|                           +------------+-----------+                            |
|    GDI via hBitmap        +------------+-----------+        +------------+      |
|    ---------------------->|                        | BitBlt |            |      |
|                           |        DIB Section     +--------+    DDB     |      |
|    ---------------------->|                        |        |            |      |
|    Directly via pBits     +------------+-----------+        +------------+      |
|                                        |                           |            |
+----------------------------------------+---------------------------+------------+
|                                  Kernel Side                       |            |
+--------------------------------------------------------------------+------------+
|                                                                    V            |
|                             Hardware Video Memory                               |
|                                                                                 |
+---------------------------------------------------------------------------------+

正如前面一直在讲DDB是依赖设备的位图,对于依赖的设备可以通过DC来获取相关信息。通过 GetDC(NULL) 可以获取计算机屏幕DC,通常这是个彩色DC,将其传入 CreateCompatibleDC() 就可以用来创建兼容的彩色DC。将其传入 CreateCompatibleBitmap() 则可以创建一个彩色位图。以下使用 GetDeviceCaps() 函数打印了一组DC的属性,关于DC后面还要深入:

HWND hwnd = GetConsoleWindow();
HDC sc = GetDC( NULL );
HDC cc = GetDC( hwnd );
HDC dc = CreateCompatibleDC(NULL);

Device Context Information:      Device Context Information:      Device Context Information:     
  TECHNOLOGY:DT_RASDISPLAY         TECHNOLOGY:DT_RASDISPLAY         TECHNOLOGY:DT_RASDISPLAY  
    HORZSIZE:482                     HORZSIZE:482                     HORZSIZE:482            
    VERTSIZE:271                     VERTSIZE:271                     VERTSIZE:271            
     HORZRES:1366                     HORZRES:1366                     HORZRES:1366           
     VERTRES:768                      VERTRES:768                      VERTRES:768            
  LOGPIXELSX:96                    LOGPIXELSX:96                    LOGPIXELSX:96             
  LOGPIXELSY:96                    LOGPIXELSY:96                    LOGPIXELSY:96             
   BITSPIXEL:32                     BITSPIXEL:32                     BITSPIXEL:32             
  NUMBRUSHES:-1                    NUMBRUSHES:-1                    NUMBRUSHES:-1             
     NUMPENS:-1                       NUMPENS:-1                       NUMPENS:-1             
   NUMCOLORS:-1                     NUMCOLORS:-1                     NUMCOLORS:-1             
 SIZEPALETTE:0                    SIZEPALETTE:0                    SIZEPALETTE:0 
 NUMRESERVED:20                   NUMRESERVED:20                   NUMRESERVED:20             
    COLORRES:24                      COLORRES:24                      COLORRES:24

  RASTERCAPS: 0x7e99 RC_BITBLT RC_BITMAP64 RC_GDI20_OUTPUT RC_DI_BITMAP RC_DIBTODEV 
              RC_BIGFONT RC_STRETCHBLT RC_FLOODFILL RC_STRETCHDIB RC_OP_DX_OUTPUT

通过检索DC的光栅能力信息 RASTERCAPS,曲线能力 CURVECAPS,直线能力 LINECAPS 等等,上面最后一行输出表示显示设备直接支持 BitBlt()、SetDIBits()、GetDIBits()。通过 RASTERCAPS 还可以查询是不是有 RC_PALETTE 调色板功能,而上面显示没有使用色板,SIZEPALETTE 和 NUMCOLORS 信息也表示没有使用色板,-1 是指最大的色值范围。这和现在使用的机器的真彩显示器是对应的,不像以前VGA显示器的色目只有几十、百个的数量,可以使用色板来映射像素的色值。

这里着重点还是在设备的调色板,当设备使用了色板,不管是显示器还是打印机还是其它任意设备,要提高DIB的显示效率,可以先将DIB转换成DDB来使用。转换过程中涉及两种方式,DIB_RGB_COLORS 和 DIB_PAL_COLORS,最简单的情况是DIB没有使用色板,前一种方式。24bpp位图就是这种情况,它没有使用色板,像素数据就是RGB色值。转换DIB时,如果设备是真彩显示器,那么就直接进行像素到像素的数据拷贝;如果设备使用了色板,那么就对DIB像素进行最接近色适配并转换为色板的索引值。最复杂的情况是DIB和设备都使用了色板,提高效率的点就在色板的匹配上。使用系统调色板 GetSystemPaletteEntries() 来创建DIB可以优化像素的传送效率,因为色板是匹配的,使用 SetDIBitsToDevice() 这样的函数就可以省略配色的过程。

在使用 SelectObject() 函数为DC选择位图对象时,CreateCompatibleBitmap() 创建的位图则会比 CreateBitmap() 创建的位图更有效率,因为前者的位图是兼容的不存在额外的色板匹配工作。色板的创建和使用相关的结构体、函数有 LOGPALETTE、CreatePalette()、RealizePalette()、SelectPalette()。详细内容可以参考 MSDN 关于 Palette Manager 部分。

GDI绘图API构架

古旧DOS平台下,绘图是通过VGA实现的,后来又有SuperVGA等等。VGA 就是 Video Graphics Array,也称为视频图形适配器 Video Graphics Adapter,它把像素存储到一个数组中即显示缓冲区。符合VGA规范的显示驱动器会定时获取缓冲区的数据,并在显示上呈现出对应图像。VGA 的应用使得DOS平台下的绘图变得方便起来,数组化的像素也方便运算来处理。VGA支持多种显示模式,从 2 色到 256 色,分辨率从 320x200 到 640x480。为了在Win32平台下使用DOS的VGA进行绘图,需要使用 DOSBOX 和 Borland C/C++ 3.1、DJGPP 2.0等等工具。设置 10h 中断相关内容如下:

INT 10h, Service 0h
Set Screen Mode

Input:   AH = 0h
         AL = Mode Number (see below)
Output:  The video mode is changed.

Mode Number  Text Res.  Graphics Res.   Description  Adapters  Max. Pages
---------------------------------------------------------------------------
         0h      40x25         ------   B&W Text     CGA+               8
         1h      80x25         ------   B&W Text     MDPA+              8
         2h      40x25         ------   Color Text   CGA+          4 or 8
         3h      80x25         ------   Color Text   (MDPA?)/CGA+  4 or 8
         4h      40x25        320x200   4 colors     CGA+               1
         5h      40x25        320x200   2 colors     CGA+               1
         6h      80x25        640x200   2 colors     CGA+               1
         7h      80x25         ------   B&W          MDPA (CGA+?)       1
         8h to Ch -- PCjr or other adapters; no longer used
         Dh      40x25        320x200   16 colors    EGA+               8
         Eh      80x25        640x200   16 colors    EGA+               4
         Fh      80x25        640x350   2 colors     EGA+               2
        10h      80x25        640x350   16 colors    EGA+               2
        11h      80x25        640x480   2 colors     VGA+               1
        12h      80x25        640x480   16 colors    VGA+               1
        13h      40x25        320x200   256 colors   VGA+               1

在 Mode 13h 即 256色模式下,每像素使用一个字节表示,总计刚好是 64K,即一个段的内存数量,它的地址约定分配在A000:0000 - A000:FFFF。而 B000:0000 则字符模式下的显示缓冲区的映射地址。通过映射显示器驱动卡的内存到计算机内存,程序可以直接通过操作计算机的内存来实现对显卡的编程,这就大大方便了图形的编程。在DOS平台下,没有大量的API要去掌握,你可以任意发挥想像,随意修改VGA接口提供的显示缓冲区来实现图形绘画,这是没有API的一个大好处。

Windows 出现后,图形编程有了统一的构架,因为需要掌握它的图形设备接口 GDI Graphics Device Interface,而大量的不开放源代码的API也成为开发人员的一种负担,可能因为文档还不太足够以完全掌握好每一个API函数。在GDI框架下,显示驱动接口映射的显示缓冲区被分割成一块块小区域分发给Windows操作系统下运行的各式各样的小窗口。而这些小块的缓冲区域是通过设备上下方对象 DC device context 来管理的,它主要负责Windows系统与绘图程序之间的信息交换,处理所有程序的图形输出。通过 GetDC() 来获取任意程序窗口对象所分配的缓冲区信息,然后对这个缓冲区进行绘画,就可以改变程序窗口的内容。当然,DC可以不跟显示器直接有关系,可以在内存中建立一个DC,然后在它上面作画,这就是离屏绘图 Off-screen DC。DC可以分为四种类型,首先是直接在显示器显示绘画的 Display DC,然后是可以在内存上作画的 Memory DC,然后是可以在打印机上打印的 Printer DC,最后是包含DC信息的 Information DC。

Memory DC是使用较多的一种,在游戏开发中,需要在内存中对图像进行操作,然后才是将图像发送到显示器上显示。通过 CreateCompatibleDC() 可以创建一个 Memory DC,在使用它进行组图之前,需要通过 SelectObject() 来设置尺寸修理工的位图,位图通过 CreateBitmap() CreateBitmapIndirect() CreateCompatibleBitmap() 就可以创建。绘图完成后就通过 BitBlt() 函数将内存DC的图像发送到显示DC上显示出来。

2001年XP系统推出时,GDI的扩展版 GDI+ 一并发布,后来 GDI+ 又被包装进.NET框架的托管类库中,成为.NET中窗体绘图的主要工具。GDI+ 主要提供了以下三类功能:

  • 矢量图形:GDI+提供了存储图形基元自身信息的类或结构体、存储图形基元绘制方式信息的类以及实际进行绘制的类;
  • 图像处理:GDI+为我们提供了Bitmap、Image等类。它们可用于显示、操作和保存BMP、JPG、GIF等图像。
  • 文字排版:GDI+支持使用各种字体、字号和样式来显示文本。

GDI接口是基于函数的,而GDI+是基于OOP Object-Orient Programming,使用起来比GDI要方便。因为GDI+实际上是GDI的封装和扩展,执行效率一般要低于GDI。

可能是 GDI+ 实在是太新鲜了,我使用 GCC 4.7.1 MinGW 移植版无法编译,即使是实例化 Graphics 类,也添加了 gdiplus.h 头文件,还是出错 Graphics 类无定义声明。然通过查看GDI+头文件,发现这货使用了命名空间,所以只需要几条指令就可以解决问题,看来是会 F**king Code 的娃:

#define ULONG_PTR ULONG 
#include <gdiplus.h>
using namespace Gdiplus;
#pragma comment(lib, "C:/gdiplus/lib/gdiplus.lib")

如果需要使用GDI+,我目前使用 MSDN 1999OCT 是找不到资料的了,回来GDI,在Windows消息系统中有一和DC相关的消息是 WM_DEVMODECHANGE,相关的GDI函数列表如下:

CancelDC                 DeviceCapabilities      GetDC               GetStockObject    
ChangeDisplaySettings    DrawEscape              GetDCBrushColor     ReleaseDC         
ChangeDisplaySettingsEx  EnumDisplayDevices      GetDCEx             ResetDC           
CreateCompatibleDC       EnumDisplaySettings     GetDCOrgEx          RestoreDC         
CreateDC                 EnumDisplaySettingsEx   GetDCPenColor       SaveDC            
CreateIC                 EnumObjects             GetDeviceCaps       SelectObject      
DeleteDC                 EnumObjectsProc         GetObject           SetDCBrushColor   
DeleteObject             GetCurrentObject        GetObjectType       SetDCPenColor     

GDI的常用图形对象有 HBITMAP、Pen、Font、Brush等,这些对象基本都在 MFC 中包装成一个个的类对象,而Windows为其定义的句柄却是通用的:

MFC Class   handle      Graphic   Associated attributes 
CBitmap     HBITMAP     Bitmap    Size, dimensions, color-format, compression and so on. 
CBrush      HBRUSH      Brush     Style, color, pattern, and origin. 
CPalette    HPALETTE    Palette   Colors and size (or number of colors). 
CFont       HFONT       Font      Typeface name, width, height, weight, character set... 
                        Path      Shape. 
CPen        HPEN        Pen       Style, width, and color. 
CRgn        HRGN        Region    Location and dimensions. 

这些都 GDI 框架的是核心类,在绘制任何图形之前,一定要先创建或得到一个GDI核心类的对象才能完成绘图工作。GDI 的图形可以理解成系统一个画图环境,它具体包括要在哪里画,画什么东西,用什么画,(颜色,画笔,画刷),怎么画,画圆还是画线等等。在MSDN上的安装盘上有大量的GDI演示例子,位置在 SAMPLES -> VC98 -> SDK -> GRAPHICS。可以使用 MSDN Library - October 1999 版,这个版本比中文版的 MSDN Library for Visual Studio 6.0 内容要丰富。关于GDI的内容主要有两部分,一是 Platform SDK 目录下的 Windows GDI 包含的全面API文档,第二部分则是来自MSDN社区的技术文章 Technical Articles,其中 Multimedia 目录下有个GDI的分类。说到MSDN,后面补充说明不同MSDN版的共享安装。

当程序创建一个DC时,系统会设置默认的对象,除了 bitmap 和 path 以外。经常和这些对象打交道的函数有 GetCurrentObject(),通过它可以获取DC上的各种图形对象的句柄,而 GetObject() 函数则功能更加强大,它可以根据不同的输入来获取诸如 BITMAP, DIBSECTION, EXTLOGPEN, LOGBRUSH, LOGFONT, LOGPEN 等等结构体对象。需要绘画不同效果的图像时,就需要为DC指定图形对象,这时就要使用 SelectObject() 函数。配套的函数还有 SetDCBrushColor() GetDCBrushColor() SetDCPenColor() GetDCPenColor()。

GDI中不同的图形对象可以有不同的工作模式,例如可以通过 SetBkMode() 函数为源位图设置背景色的透明混合方式,这样GDI函数在混合图像时就可以得到透明的效果。尽管MSDN文档中只说明了 OPAQUE 和 TRANSPARENT 两种参数选项,但是 NEWTRANSPARENT 是另一个可以用来处理透明效果的选项。通过设置透明模式,再通过 SetBkColor() 为源位图设置一个背景色,像 StretchDIBits() 这样的函数就会将背景色当作透明色来处理。模式设置是GDI中的重要组成,通过不同的模式可以实现不同的绘图效果,相关的API如下可以查阅MSDN:

Graphics mode   Get Function       Set Function     
Background      GetBkMode          SetBkMode        
Drawing         GetROP2            SetROP2          
Mapping         GetMapMode         SetMapMode       
Polygon-fill    GetPolyFillMode    SetPolyFillMode  
Stretching      GetStretchBltMode  SetStretchBltMode

句柄那一套

前面看了这么多函数,它们基本都返回了一个指向由系统管理着的对象,比如说我现在最关心的 HBITMAP 句柄,实际上我更希望它是指向位图文件数据在内存的位置,而不是一个所谓“句柄”的东西,这种感觉不好。

通过 windef.h 头文件可以找到句柄的类型链:

DECLARE_HANDLE(HBITMAP);

#ifdef STRICT
    typedef void *HANDLE;
    #define DECLARE_HANDLE(name) struct name##__ { int unused; }; typedef struct name##__ *name
#else
    typedef PVOID HANDLE;
    #define DECLARE_HANDLE(name) typedef HANDLE name
#endif

其中 ## 是一个内置宏定义,意思是在预处理时的字符串的连接。经过编译器的预处理后,HBITMAP 就会成为这两个样子:

#ifdef STRICT
    typedef void *HANDLE;
    struct HBITMAP__ { int unused; };
    typedef struct HBITMAP__ *HBITMAP;
#else
    typedef PVOID HANDLE;
    typedef HANDLE HBITMAP;
#endif

生成的代码有两种形式,在开严格模式 STRICT 时,定义了一个只有一个整形数据的结构体,而句柄就是指向这个结构体的指针,另一种情况则定义了一个无类型的指针。

句柄这个东西到底如何理解呢,我觉得作为Windows系统开发团队之外的人,应该从几个方面来看,从语言标准上,句柄就是指针和结构体的组合,这一点是基本的理解。从系统的结构层次来看,句柄是系统管理虚拟内存的一种方法。在《深入x86的内存寻址》提到,现有的32位x86构架CPU可以寻址4GB内存,而实际上当前还有大量机器根本没有配备这么多的内存。为了让程序有这个寻址能力,Windows采用的是虚拟内存管理技术,通过CPU的内存分页机制,将磁盘空间映射为虚拟内存,因此这个4GB的寻址空间称为 Virtual Address Space,代表机器并不是真的一定要有4GB内存。通过虚拟内存,将一些不太可能使用的内存数据移动到磁盘上就可以节省出物理内存,这样就可以为在运行的程序提供多多可用的内存,因此磁盘上映射到内存的文件也称为页交换文件。

<
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值