1. 基本概念
先来用通俗的语句讲解位图和调色板的概念。
我们知道,自然界中的所有颜色都可以由红、绿、蓝(R,G,B)三基色组合而成。针对含有红、绿、蓝色成分的多少,可以对其分别分成0~255个等级,而红、绿、蓝的不同组合共有256×256×256种,因此约能表示1600万种颜色。对于人眼而言,这已经是"真彩色"了。
对每个像素进行了(R,G,B)量化的图像就是位图,其在计算机中对应文件的扩展名一般为.bmp。既然用R,G,B的量化值就可以直接记录一张位图的所有像素,那我们需要调色板干什么呢?
首先,我们可以计算完全利用(R,G,B)组合来存储一个800×600的位图所需要的空间为:
800×600×3 = 1440000(字节)= 1.37M(字节)
惊人的大!因此,调色板横空出世了,它的功能在于缓解位图文件存储空间过大的问题。
假设一个位图为16色,其像素总数为800×600。我们只需要用4个bit就可以存储这个位图的每个像素在16种颜色中所处的等级,然后调色板提供了这16种等级对应的(R,G,B)值,这样,存储这个16色位图只需要:
800×600×4/8 = 240000(字节)= 0.22 M(字节)
额外的存储R,G,B表的开销(即调色板Palette,也称为颜色查找表LUT)仅仅为16×3=48字节。
存储空间被大为减少!
常见的位图有单色、16色、256色、16位及24位真彩色5种,对于前三者(即不大于256色)都可以调色板方式进行存储,而对16位及24位真彩色以调色板进行存储是不划算的,它们直接按照R,G,B分量进行存储。
在此基础上我们来分析DDB位图(Device-dependent bitmap,与设备相关的位图)与DIB位图(Device-independent bitmap,与设备无关的位图)的概念以及二者的区别。
DDB依赖于具体设备,它只能存在于内存中(视频内存或系统内存),其颜色模式必须与特定的输出设备相一致,使用系统调色板。一般只能载入色彩较简单的DDB位图,对于颜色较丰富的位图,需使用DIB才能长期保存。
DIB不依赖于具体设备,可以用来永久性地保存图象。DIB一般是以*.BMP文件的形式保存在磁盘中的,有时也会保存在*.DIB文件中。 DIB位图的特点是将颜色信息储存在位图文件自身的颜色表中,应用程序要根据此颜色表为DIB创建逻辑调色板。因此,在输出一幅DIB位图之前,程序应该将其逻辑调色板选入到相关的设备上下文并实现到系统调色板中。
2. 例程简述
本文后续的讲解都基于这样的一个例子工程,它是一个基于对话框的MFC应用程序,包括2个父菜单:
(1) DDB位图
DDB位图父菜单又包括两个子菜单:
a. ID:IDM_LOADDDBPIC caption:加载
单击事件:加载资源中的DDB位图并显示之
b. ID:IDM_MARK_DDBPIC caption:标记
单击事件:在DIB位图中透明地添加天极网logo
(2) DIB位图
DIB位图父菜单又包括两个子菜单:
a. ID:IDM_OPENDIBPIC caption:打开
单击事件:弹出文件对话框,打开.bmp位图文件,并显示
b. ID:IDM_MARK_DIBPIC caption:标记
单击事件:在DIB位图中透明地添加天极网logo
工程中还包含下列位图资源:
(1)IDB_LOADED_BITMAP:要加载的位图资源
(2)IDB_YESKY_BITMAP:天极网logo
后续篇章将集中在对4个子菜单单击事件消息处理函数的讲解,下面的代码是整个对话框类CBitMapExampleDlg的消息映射:
BEGIN_MESSAGE_MAP(CBitMapExampleDlg, CDialog)
//{{AFX_MSG_MAP(CBitMapExampleDlg)
ON_WM_SYSCOMMAND()
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
ON_COMMAND(IDM_LOADDDBPIC, OnLoadddbpic)
ON_COMMAND(IDM_MARK_DDBPIC, OnMarkDdbpic)
ON_COMMAND(IDM_OPENDIBPIC, OnOpendibpic)
ON_COMMAND(IDM_MARK_DIBPIC,OnMarkDibpic) //}}AFX_MSG_MAP
END_MESSAGE_MAP()
3. DDB位图编程
先看DDB加载按钮的单击事件代码:
void CBitMapExampleDlg::OnLoadddbpic()
{
1: CBitmap bmpDraw;
2: bmpDraw.LoadBitmap( IDB_LOADED_BITMAP );//装入要加载的DDB位图
3: BITMAP bmpInfo;
4: bmpDraw.GetBitmap( &bmpInfo ); //获取要加载DDB位图的尺寸
5: CDC memDC;//定义一个兼容DC
6: CClientDC dc( this );
7: memDC.CreateCompatibleDC( &dc );//创建兼容DC
8: CBitmap* pbmpOld = memDC.SelectObject( &bmpDraw );//保存原有DDB,并选入新DDB入DC
9: dc.BitBlt( 0, 0, bmpInfo.bmWidth, bmpInfo.bmHeight, &memDC, 0, 0, SRCCOPY );
10: memDC.SelectObject( pbmpOld );//选入原DDB
}
上述代码将产生如图1所示的效果,位图被安置在对话框(0,0)坐标开始的位置上。
图1 加载DDB位图资源
我们来逐行解析上述代码是怎样产生图1的效果的。
第1、2行定义了一个CBitmap对象,并调用其成员函数LoadBitmap加载工程中的位图资源IDB_LOADED_BITMAP。第3、4行定义了BITMAP结构体的实例并调用CBitmap的成员函数GetBitmap获得位图信息,BITMAP结构体定义在头文件中,其形式为:
/* Bitmap Header Definition */
typedef struct tagBITMAP
{
LONG bmType; //必需为0
LONG bmWidth; //位图的宽度(以像素为单位)
LONG bmHeight; //位图的高度(以像素为单位)
LONG bmWidthBytes; //每一扫描行所需的字节数,应是偶数
WORD bmPlanes; //色平面数
WORD bmBitsPixel; //色平面的颜色位数
LPVOID bmBits; //指向存储像素阵列的数组
} BITMAP, *PBITMAP, NEAR *NPBITMAP, FAR *LPBITMAP;
第5~8行的作用是:构建一个CDC对象,调用CDC::CreateCompatibleDC创建一个兼容的内存设备上下文,接着调用CDC::SelectObject将DDB选入内存设备上下文中。
第9行调用函数CDC::BitBlt绘制位图,CDC::BitBlt的原型为:
CDC::BitBlt(int x, int y, int nWidth, int nHeight, CDC *pSrcDC, int xSrc, int ySrc, DWORD dwRop)
CDC::BitBlt执行的操作为将源DC中位图复制到目的DC中。其中前四个参数为目的区域的坐标(x,y)及长度和宽度(Width, nHeight),第五个参数是源DC指针,接下来的参数是源DC中的起始坐标,最后一个参数为光栅操作的类型。
第10行调用CDC::SelectObject把原来的DDB选入到内存设备上下文中并使新DDB脱离出来。
与CDC::BitBlt对应的还有另一个函数CDC::StretchBlt,它具有缩放功能,其原型为:
BOOL CDC::StretchBlt(int x, int y, int nWidth, int nHeight, CDC *pSrcDC, int
xSrc, int ySrc, int nSrcWidth, int nSrcHeight, DWORD dwRop);
该函数把位图从源矩形拷贝到目的矩形中,如果源和目的矩形尺寸不同,那么将缩放位图的功能以适应目的矩形的大小。函数的大部分参数与BitBlt的相同,但多了两个参数nSrcWidth和nSrcHeight用来指定源矩形的宽和高。
如果我们将函数CBitMapExampleDlg::OnLoadddbpic() 中的第9行改为:
CRect clientRect;
GetClientRect(&clientRect); //获得对话框窗口的大小
dc.StretchBlt(0, 0, clientRect.right, clientRect.bottom, &memDC, 0, 0,
bmpInfo.bmWidth, bmpInfo.bmHeight, SRCCOPY);
则单击加载按钮后的对话框如图2所示,位图被拉伸至整个对话框的范围。
图2 拉伸位图
CDC::BitBlt和dc.StretchBlt函数中的dwRop参数较为有用,它定义光栅操作的类型。请看"DDB位图"父菜单下"标记"子菜单单击事件的消息处理函数代码:
void CBitMapExampleDlg::OnMarkDdbpic()
{
CBitmap bmpDraw;
bmpDraw.LoadBitmap(IDB_YESKY_BITMAP); //装入天极网logo DDB位图资源
BITMAP bmpInfo;
bmpDraw.GetBitmap(&bmpInfo); //获取天极网logo位图的尺寸
CDC memDC; //定义一个兼容DC
CClientDC dc(this);
memDC.CreateCompatibleDC(&dc); //创建DC
CBitmap *pbmpOld = memDC.SelectObject(&bmpDraw);
//保存原有DDB,并选入天极网logo位图入DC
dc.BitBlt(0, 0, bmpInfo.bmWidth, bmpInfo.bmHeight, &memDC, 0, 0, SRCAND);
memDC.SelectObject(pbmpOld); //选入原DDB
}
单击该按钮后,将产生如图3的效果,天极网的logo被透明地添加到了位图中!
图3 在DDB位图中加入天极网logo
能产生这个效果的原因在于我们在代码行:
dc.BitBlt ( 0, 0, bmpInfo.bmWidth, bmpInfo.bmHeight, &memDC, 0, 0, SRCAND );
中使用了参数SRCAND(不同于先前代码中SRCCOPY,它仅仅意味着复制源位图到目的位图),它的含义为源和目的间进行AND操作。我们不知道天极网的编辑同志是怎么为文章中的图片加logo的,有可能他们就使用了具有自动AND功能的图像加logo批处理软件。的确,我们可以利用例程中的原理写一个批处理软件,一次对一堆图片自动添加logo。
参数dwRop除了可以为SRCAND和SRCCOPY外,还可以有如下取值:
BLACKNESS:输出区域为黑色
DSTINVERT:反转目的位图
MERGECOPY:用与操作把图案(Pattern)与源位图融合起来
MERGEPAINT:用或操作把反转的源位图与目的位图融合起来
NOTSRCCOPY:把源位图反转然后拷贝到目的地
NOTSRCERASE:用或操作融合源和目的位图,然后再反转
PATCOPY:把图案拷贝到目的位图中
PATINVERT:用异或操作把图案与目的位图相融合
PATPAINT:用或操作融合图案和反转的源位图,然后用或操作把结果与目的位图融合
SRCERASE:先反转目的位图,再用与操作将其与源位图融合
SRCINVERT:用异或操作融合源位图和目的位图
SRCPAINT:用或操作融合源位图和目的位图
WHITENESS:输出区域为白色
合理利用这些取值将帮助我们制作出特定要求的图像处理软件。
从上述实例我们可以看出,在VC中使用CBitmap类,必须将位图放入工程的资源中,并使用类 CBitmap的成员函数LoadBitmap加载之,再通过CDC类的成员函数BitBlt进行DC拷贝等操作达到显示的目的。CBitmap有显示的不足:
(1) 位图需要放入工程资源中,这将导致工程的可执行文件变大;
(2) 因为位图需放入工程资源中,而资源中不能无穷无尽地包含位图,应用程序无法自适应地选取其它位图,能使用的位图十分有限的;
(3) 类CBitmap只是DDB位图操作API的封装,不能独立于平台。
DIB位图则可以解决上述问题,其特点是以.BMP位图文件格式存储独立于平台的图像数据,下面我们来详细分析。
4. DIB位图编程
4.1位图文件格式
先来分析DIB位图文件的格式。位图文件分为四部分:
(1)位图文件头BITMAPFILEHEADER
位图文件头BITMAPFILEHEADER是一个结构体,长度为14字节,定义为:
1
2
3
4
5
6
7
8typedef struct tagBITMAPFILEHEADER { WORD bfType; //文件类型,必须是0x424D,即字符串"BM" DWORD bfSize; //文件大小,包括BITMAPFILEHEADER的14个字节 WORD bfReserved1; //保留字 WORD bfReserved2; //保留字 DWORD bfOffBits; //从文件头到实际的位图数据的偏移字节数 } BITMAPFILEHEADER;
(2)位图信息头BITMAPINFOHEADER
位图信息头BITMAPINFOHEADER也是一个结构体,长度为40字节,定义为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17typedef struct tagBITMAPINFOHEADER { DWORD biSize; //本结构的长度,为40 LONG biWidth; //图象的宽度,单位是象素 LONG biHeight; //图象的高度,单位是象素 WORD biPlanes; //必须是1 WORD biBitCount; //表示颜色时要用到的位数,1(单色), 4(16色), 8(256色), 24(真彩色) DWORD biCompression; //指定位图是否压缩,有效的值为BI_RGB,BI_RLE8,BI_RLE4,BI_BITFIELDS等,BI_RGB表示不压缩 DWORD biSizeImage; //实际的位图数据占用的字节数,即 biSizeImage=biWidth’ × biHeight,biWidth’是biWidth 按照4的整倍数调整后的结果 LONG biXPelsPerMeter; //目标设备的水平分辨率,单位是每米的象素个数 LONG biYPelsPerMeter; //目标设备的垂直分辨率,单位是每米的象素个数 DWORD biClrUsed; //位图实际用到的颜色数,0表示颜色数为2biBitCount DWORD biClrImportant; //位图中重要的颜色数,0表示所有颜色都重要 } BITMAPINFOHEADER;
(3)调色板Palette
调色板Palette针对的是需要调色板的位图,即单色、16色和256色位图。对于不以调色板方式存储的位图,则无此项信息。调色板是一个数组,共有biClrUsed个元素(如果该值为0,则有2biBitCount个元素)。数组中每个元素是一个RGBQUAD结构体,长度为4个字节,定义为:
1
2
3
4
5
6
7typedef struct tagRGBQUAD { BYTE rgbBlue; //蓝色分量 BYTE rgbGreen; //绿色分量 BYTE rgbRed; //红色分量 BYTE rgbReserved; //保留值 } RGBQUAD;
(4)实际的位图数据ImageDate
对于用到调色板的位图,实际的图象数据ImageDate为该象素的颜色在调色板中的索引值;对于真彩色图,图象数据则为实际的R、G、B值:
a.单色位图:用1bit就可以表示象素的颜色索引值;
b.16色位图:用4bit可以表示象素的颜色索引值;
c. 256色位图:1个字节表示1个象素的颜色索引值;
d.真彩色:3个字节表示1个象素的颜色R,G,B值。
此外,位图数据每一行的字节数必须为4的整倍数,如果不是,则需要补齐。奇怪的是,位图文件中的数据是从下到上(而不是从上到下)、从左到右方式存储的。
4.2位图的显示
Visual C++ MFC中没有提供一个专门的类来处理DIB位图,因此,为了方便地使用位图文件,我们有必要派生一个CDib类。类的源代码如下:
(1) CDib类的声明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209// DIB.h:类CDib声明头文件 #ifndef __DIB_H__ #define __DIB_H__ #include class CDib { public: CDib(); ~CDib(); BOOL Load( const char * ); BOOL Save( const char * ); BOOL Draw( CDC *, int nX = 0, int nY = 0, int nWidth = -1, int nHeight = -1, int mode = SRCCOPY); BOOL SetPalette( CDC * ); private: CPalette m_Palette; unsigned char *m_pDib, *m_pDibBits; DWORD m_dwDibSize; BITMAPINFOHEADER *m_pBIH; RGBQUAD *m_pPalette; int m_nPaletteEntries; }; #endif (2) CDib类的实现 // DIB.cpp:类CDib实现文件 #include "stdafx.h" #include "DIB.h" CDib::CDib() { m_pDib = NULL; } CDib::~CDib() { // 如果位图已经被加载,释放内存 if (m_pDib != NULL) delete []m_pDib; } 下面这个函数非常重要,其功能为加载位图,类似于CBitmap类的LoadBitmap函数: BOOL CDib::Load(const char *pszFilename) { CFile cf; // 打开位图文件 if (!cf.Open(pszFilename, CFile::modeRead)) return (FALSE); // 获得位图文件大小,并减去BITMAPFILEHEADER的长度 DWORD dwDibSize; dwDibSize = cf.GetLength() - sizeof(BITMAPFILEHEADER); // 为DIB位图分配内存 unsigned char *pDib; pDib = new unsigned char[dwDibSize]; if (pDib == NULL) return (FALSE); BITMAPFILEHEADER BFH; // 读取位图文件数据 try { // 文件格式是否正确有效 if ( cf.Read(&BFH, sizeof(BITMAPFILEHEADER)) != sizeof(BITMAPFILEHEADER) || BFH.bfType != ’MB’ || cf.Read(pDib, dwDibSize) != dwDibSize) { delete []pDib; return (FALSE); } } catch (CFileException *e) { e->Delete(); delete []pDib; return (FALSE); } // delete先前加载的位图 if (m_pDib != NULL) delete m_pDib; // 将临时Dib数据指针和Dib大小变量赋给类成员变量 m_pDib = pDib; m_dwDibSize = dwDibSize; // 为相应类成员变量赋BITMAPINFOHEADER和调色板指针 m_pBIH = (BITMAPINFOHEADER*)m_pDib; m_pPalette = (RGBQUAD*) &m_pDib[sizeof(BITMAPINFOHEADER)]; // 计算调色板中实际颜色数量 m_nPaletteEntries = 1 << m_pBIH->biBitCount; if (m_pBIH->biBitCount >8) m_nPaletteEntries = 0; else if (m_pBIH->biClrUsed != 0) m_nPaletteEntries = m_pBIH->biClrUsed; // 为相应类成员变量赋image data指针 m_pDibBits = &m_pDib[sizeof(BITMAPINFOHEADER) + m_nPaletteEntries * sizeof (RGBQUAD)]; // delete先前的调色板 if (m_Palette.GetSafeHandle() != NULL) m_Palette.DeleteObject(); // 如果位图中存在调色板,创建LOGPALETTE 及CPalette if (m_nPaletteEntries != 0) { LOGPALETTE *pLogPal = (LOGPALETTE*)new char[sizeof(LOGPALETTE) + m_nPaletteEntries *sizeof(PALETTEENTRY)]; if (pLogPal != NULL) { pLogPal->palVersion = 0x300; pLogPal->palNumEntries = m_nPaletteEntries; for (int i = 0; i < m_nPaletteEntries; i++) { pLogPal->palPalEntry[i].peRed = m_pPalette[i].rgbRed; pLogPal->palPalEntry[i].peGreen = m_pPalette[i].rgbGreen; pLogPal->palPalEntry[i].peBlue = m_pPalette[i].rgbBlue; } //创建CPalette并释放LOGPALETTE的内存 m_Palette.CreatePalette(pLogPal); delete []pLogPal; } } return (TRUE); } //函数功能:保存位图入BMP文件 BOOL CDib::Save(const char *pszFilename) { if (m_pDib == NULL) return (FALSE); CFile cf; if (!cf.Open(pszFilename, CFile::modeCreate | CFile::modeWrite)) return (FALSE); try { BITMAPFILEHEADER BFH; memset(&BFH, 0, sizeof(BITMAPFILEHEADER)); BFH.bfType = ’MB’; BFH.bfSize = sizeof(BITMAPFILEHEADER) + m_dwDibSize; BFH.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + m_nPaletteEntries *sizeof(RGBQUAD); cf.Write(&BFH, sizeof(BITMAPFILEHEADER)); cf.Write(m_pDib, m_dwDibSize); } catch (CFileException *e) { e->Delete(); return (FALSE); } return (TRUE); } 下面这个函数也非常重要,其功能为在pDC指向的CDC中绘制位图,起点坐标为(nX,nY),绘制宽度和高度为nWidth、nHeight,最后一个参数是光栅模式: BOOL CDib::Draw(CDC *pDC, int nX, int nY, int nWidth, int nHeight, int mode) { if (m_pDib == NULL) return (FALSE); // 获取位图宽度和高度赋值 if (nWidth == - 1) nWidth = m_pBIH->biWidth; if (nHeight == - 1) nHeight = m_pBIH->biHeight; // 绘制位图 StretchDIBits(pDC->m_hDC, nX, nY, nWidth, nHeight, 0, 0, m_pBIH->biWidth, m_pBIH->biHeight, m_pDibBits, (BITMAPINFO*)m_pBIH, BI_RGB, mode); return (TRUE); } //函数功能:设置调色板 BOOL CDib::SetPalette(CDC *pDC) { if (m_pDib == NULL) return (FALSE); // 检查当前是否有一个调色板句柄,对于大于256色的位图,为NULL if (m_Palette.GetSafeHandle() == NULL) return (TRUE); // 选择调色板,接着实施之,最后恢复老的调色板 CPalette *pOldPalette; pOldPalette = pDC->SelectPalette(&m_Palette, FALSE); pDC->RealizePalette(); pDC->SelectPalette(pOldPalette, FALSE); return (TRUE); } 从整个CDib类的代码中我们可以看出,DIB位图的显示需遵循如下步骤: (1)读取位图,本类中使用pDib = new unsigned char[dwDibSize]为位图中的信息分配内存,另一种方法是调用API函数CreateDIBSection,譬如: m_hBitmap = ::CreateDIBSection(pDC->GetSafeHdc(), (LPBITMAPINFO) m_lpBMPHdr, DIB_RGB_COLORS, (LPVOID*) &m_lpDIBits, NULL, 0); m_hBitmap定义为: HBITMAP m_hBitmap; (2)根据读取的位图信息,计算出调色板大小,然后创建调色板; (3)调用CDib::SetPalette( CDC *pDC )设置调色板,需要用到CDC::SelectPalette及CDC::RealizePalette两个函数; (4)调用CDib::Draw(CDC *pDC, int nX, int nY, int nWidth, int nHeight, int mode)函数绘制位图。在此函数中,真正发挥显示位图作用的是对StretchDIBits API函数的调用。StretchDIBits函数具有缩放功能,其最后一个参数也是光栅操作的模式。 下面给出DIB位图的打开及显示并在其中加入天极网logo的函数源代码。"DIB位图"父菜单下"打开"子菜单的单击事件消息处理函数为(其功能为打开位图并显示之): void CBitMapExampleDlg::OnOpendibpic() { // 弹出文件对话框,让用户选择位图文件 CFileDialog fileDialog(TRUE, "*.BMP", NULL, NULL,"位图文件(*.BMP)|*.bmp;*.BMP|"); if (IDOK == fileDialog.DoModal()) { // 加载位图并显示之 CDib dib; if (dib.Load(fileDialog.GetPathName())) { CClientDC dc(this); dib.SetPalette(&dc); dib.Draw(&dc); } } } "DIB位图"父菜单下"标记"子菜单的单击事件消息处理函数为(其功能为给位图加上天极网logo): void CBitMapExampleDlg::OnMarkDibpic() { // 弹出文件对话框,让用户选择标记logo CFileDialog fileDialog(TRUE, "*.BMP", NULL, NULL, "标记位图文件(*.BMP)|*.bmp;*.BMP|"); if (IDOK == fileDialog.DoModal()) { // 加载标记logo位图并与目标位图相与 CDib dib; if (dib.Load(fileDialog.GetPathName())) { CClientDC dc(this); dib.SetPalette(&dc); dib.Draw(&dc, 0, 0, - 1, - 1, SRCAND); } } }
图4显示了DIB位图加载天极网logo后的效果,要好于图3中加天极网logo后的DDB位图。图4显示的是真彩色位图相互与的结果,而图3中的图像颜色被减少了。
图4 在DIB位图中加入天极网logo
5. 结束语
本文介绍了位图及调色板的概念,并讲解了DDB位图与DIB位图的区别。在此基础上,本文以实例讲解了DDB位图和DIB位图的操作方式。DDB位图的处理相对比较简单,对于DIB位图,我们需要定义一个MFC所没有的新类CDib,它屏蔽位图信息的读取及调色板创建的技术细节,应用程序可以方便地使用之。
如何创建位图(VC版)
这篇文章介绍了一种创建位图的方法以及如何在位图中绘图并保存成为位图文件。位图本质上是视频图像的内存表示,它做为windows的一种资源可以用于很多场合,被几乎所有的绘图软件所支持。象图标、墙纸、图形、图像都可以以位图的形式来存储。在这里我们讨论的位图是属于DIB(device-independent-bitmap)文件格式,我们称之为与设备环境(在这里可以称为显示设备)无关的位图。位图文件可以分为三部分来组成(有些人把它分为四部分):
一、 文件头(BITMAPFILEHEADER):
我们看文件头的数据结构:
typedef struct tagBITMAPFILEHEADER {
WORD bfType;// 用来指定文件类型,其值必须为BM即:0x424d。
DWORD bfSize;//以字节为单位指定位图文件的大小。
WORD bfReserved1;//保留,但其值必须为0。
WORD bfReserved2; //保留,但其值必须为0。
DWORD bfOffBits;///以字节为单位的从文件头到位图数据的偏移量
} BITMAPFILEHEADER;
二、 位图信息(BITMAPINFO):
我们看位图信息的数据结构:
typedef struct tagBITMAPINFO {
BITMAPINFOHEADER bmiHeader;/*信息头结构,包含位图的尺寸和颜色格式。对于不同系统版本,我们有不同的信息头结构版本:BITMAPINFOHEADER用于NT3.51及老的版本, BITMAPV4HEADER用于NT4.0和WIN95版本,BITMAPV5HEADER用于NT5.0和WIN98版本。*/
RGBQUAD bmiColors[1];///调色板结构,用来存放颜色的。常以数组(也常常称///为调色表)形式出现。
} BITMAPINFO
我们看信息头结构BITMAPINFOHEADER:
typedef struct tagBITMAPINFOHEADER{
DWORD biSize; ///此结构所需要的字节数。
LONG biWidth; ///以像素为单位的位图的宽度。
LONG biHeight; ///以像素为单位的位图的高度。见详细说明一。
WORD biPlanes; ///指定目标设备的平面数,目前这个值必须设置为1。
WORD biBitCount ///指定每个像素点所需要的位的个数。见详细说明二。
DWORD biCompression; ///指定压缩的类型。见详细说明三。
DWORD biSizeImage; ///以字节位单位的图像大小。见详细说明四。
LONG biXPelsPerMeter;///以像素/米为单位来说明水平分辨率。
LONG biYPelsPerMeter; ///以像素/米为单位来说明垂直分辨率。
DWORD biClrUsed; ///规定了位图中使用调色表索引的数目。一般此值为0,///表示可使用所有的颜色索引。
DWORD biClrImportant; ///指定调色表的一个索引,这个索引所代表的颜色///将做为显示位图需要的很重要的颜色。如果0代表所///有索引。
} BITMAPINFOHEADER;
详细说明一:
如果这个高度值是正数(>0),这个位图的原点坐标位于左下角(意味着位图的方向是倒的),如果是负数,这个位图的原点坐标位于左上角(意味着位图的方向是正的)。
详细说明二:
这个值指定了在位图中一个像素点用多少个位(bit)来表示。位数必须是下面值的一种:
1、 0。意味着jpeg格式。只能够在win98,NT5.0或更高的版本上使用。
2、 1。每个像素点需要一个位。两种颜色,缺省的是黑色和白色。在这种情况下,位图数据中的每一个位表示一个像素。如果这个位是0,那么这个像素就用调色板表的第一个索引所对应的颜色值来表示;如果这个位是1,那么这个像素就用调色表中的第二个索引所对应的颜色值来表示。
3、 4。每个像素点需要四个位。最多有十六种颜色。每一个像素可以用在调色表中从0到15的索引所对应的颜色值来表示。举例来讲,在位图数据种的第一个字节如果是0x1F,那么这两个像素的第一个像素所包含的颜色在调色表中由索引值为1的索引所代表的颜色组成;而第二个像素所包含的颜色在调色表中由索引值为15的索引所代表的颜色组成。
4、8。每个像素点需要八个位。最多有256种颜色。每一个像素可以用在调色表中从0到255的索引所对应的颜色值来表示。因为8个位即为一个字节,所以每个像素点由单个字节组成。举例而言,在位图数据中如果有某一个数据为OxFF,那么这个像素点所包含的颜色在调色表中由索引值为255的索引所代表的颜色组成。
8、16。用两个字节(16bit)来表示一个像素点。最多有65536(2^16)种颜色。如果biCompression的值是BI_RGB,那么BITMAPINFO 的bmiColors成员须为NULL值。红、绿、蓝三基色都用5个位(bit)来表示,这三基色的位的顺序为:0(不用)00000(红)00000(绿)00000(蓝),即由小到大分别为蓝、绿、红。
9、24。用24个位来表示一个像素点。 最多有16777216(2^24)种颜色。BITMAPINFO 的bmiColors成员必须为NULL值。在位图数据中,每三个字节一组用来表示一个像素点。
10、32。用32个位表示一个像素点。最多有4294967296(2^32)种颜色。如果biCompression的值是BI_RGB,那么BITMAPINFO 的bmiColors成员须为NULL值。
详细说明三:
压缩仅仅对于倒向(左下角坐标的位图)的位图有效。可以有下面的选择:
1、 BI_RGB:不压缩的格式。
2、 BI_RLE8:8位每像素点的RLE格式。
3、 BI_RLE4:4位每像素点的RLE格式。
4、 BI_BITFIELDS:位图不压缩,对于16位和32位位图有效。
5、 BI_JPEG:对于win98、NT5.0或较高的版本有效。指示这个图像是一个JPEG的文件格式。
详细说明四:
如果是BI_RGB格式,其值可以设为零。如果biCompressionshi是JBI_JPEG,则:其值指的是jpeg图像缓冲的大小。
我们看调色板结构RGBQUAD:
typedef struct tagRGBQUAD {
BYTE rgbBlue; ///指定蓝色的深度
BYTE rgbGreen; ///指定绿色的深度
BYTE rgbRed; ///指定红色的深度
BYTE rgbReserved; ///保留,暂时不用
} RGBQUAD;
第三部分(DATA CONTENT):
这部分存放位图的图像数据。
仅仅创建一个位图,大部分情况不是我们编程的目的,在位图中绘出我们所需要的东西,才是我们真正需要的。WINDOWS提供了专门的绘图对象如画刷(填充颜色)、画笔(画点、线)及其它的GDI对象如:字体、矩形等等。有了这些对象我们就可以在位图中画出自己想要的图,并保存为位图文件。生成一个位图文件,大体上可以分为以下几个步骤:
一、 根据自己的需要初始化位图信息结构BITMAPINFO,同时建立设备环境(显示设备环境)和与此设备环境逻辑兼容的内存设备环境。内存设备环境对我们很重要,在本文中几乎所有的绘图操作都是针对它而言的。还好windows提供了CreateCompatibleDC这个函数,用它可以建立内存设备环境对象。
二、 建立自己的位图对象或位图句柄(可以利用CreateDIBSection函数),并将其选进(或者说关联到)内存设备环境上,以后我们就可以通过内存设备环境来对位图操作了。
三、 建立自己需要的绘图工具,如画刷、画笔并选择它们的颜色。以及其它所需要的GDI对象如字体、矩形等等。
四、 通过内存设备环境选择自己将要用的绘图工具,要选择绘图工具(对象)SelectObject这个函数(或者说成员)是我们最佳的选择。
五、 利用绘图工具开始绘图,如画线,画点、画矩形、画圆等等,以及填充某些区域。在这个过程中我们可以根据需要更换自己不同颜色的画笔和画刷。这好像一个画家当他画太阳时用红色的画笔,然后用红色的画刷去填充,画蓝天时就用蓝色的画笔,然后用蓝色的画刷去填充。
六、 当所有的绘图操作完成以后,开始对位图进行保存,因此我们要建立和初始化位图文件头结构BITMAPFILEHEADER,为建立一个位图文件做准备。当然需要保存的文件格式必须严格按照位图(bmp)的文件格式,即:文件头、位图信息和图像数据。对于图像数据我们可以通过位图数据的入口指针(由函数CreateDIBSection的第四个参数*ppvBits
确定)来获得。
下面给出一个完整的实例函数(仅供参考),在VC5.0上编译通过,其目的用来绘出某一股票的指数K线图(在内存设备环境上),并将其保存为位图文件(可以在本人个人信息中看到生成的图片)。
void CImage::MakeIndex(TIndex *pIndex,TDate &pDate,const TCHAR *pchTitle,int iDays,int iRectWidth)
{
//* Create a file about bmp and Create BITMAPINFOHEADER First.
LPBITMAPINFO lpbmih = new BITMAPINFO;
lpbmih->bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
lpbmih->bmiHeader.biWidth = m_iWidth;
lpbmih->bmiHeader.biHeight = m_iHeight;
lpbmih->bmiHeader.biPlanes = 1;
lpbmih->bmiHeader.biBitCount = g_iPixel;
lpbmih->bmiHeader.biCompression = BI_RGB;
lpbmih->bmiHeader.biSizeImage = 0;
lpbmih->bmiHeader.biXPelsPerMeter = 0;
lpbmih->bmiHeader.biYPelsPerMeter = 0;
lpbmih->bmiHeader.biClrUsed = 0;
lpbmih->bmiHeader.biClrImportant = 0;
//* Create Data for bmp
HDC hdc,hdcMem;
HBITMAP hBitMap = NULL;
CBitmap *pBitMap = NULL;
CDC *pMemDC = NULL;
BYTE *pBits;
CBrush brWhite,brBlack;
hdc = CreateIC(TEXT("DISPLAY"),NULL,NULL,NULL);
hdcMem = CreateCompatibleDC(hdc);
hBitMap = CreateDIBSection(hdcMem,lpbmih,DIB_PAL_COLORS,(void **)&pBits,NULL,0);
pBitMap = new CBitmap;
pBitMap->Attach(hBitMap);
pMemDC = new CDC;
pMemDC->Attach(hdcMem);
pMemDC->SelectObject(pBitMap);
brWhite.CreateSolidBrush(RGB(255,255,255));
brBlack.CreateSolidBrush(RGB(0,0,0));
//* Clear First and draw coordinate with gray.
pMemDC->FillRect(CRect(0,0,m_iWidth,m_iHeight),&brWhite);
int iGray = RGB(192,192,192),iDrakGray = RGB(128,128,128),iDateHeight = 11; //* Color Gray and eg.2002-3-8's and 2-11M's and MinIndex's and MaxIndex's Height.
int iSpaceLine = 12; //* The space with two lines of bottom.
int iTitleHeight; //* eg.上海A股's Height.
TEXTMETRIC ptm;
CRect rect;
CPen newPenDot(PS_DASH,1,iDrakGray);
CPen penDrakGray(PS_SOLID,1,iDrakGray);
CPen penGray(PS_SOLID,1,iGray);
GetTextMetrics(hdcMem,&ptm);
iTitleHeight = ptm.tmHeight;
pMemDC->SelectObject(&penDrakGray);
//* Draw K line for Index.
int iOffsetClose = -1;
for(int i=0; i<iDays; i++)
{
if(pIndex[i].fOpen <= pIndex[i].fClose) //* 阳线
{
//* Up-shadow line.
pMemDC->MoveTo(pIndex[i].iDay,pIndex[i].fClose); //* iRectWidth equate 2
pMemDC->LineTo(pIndex[i].iDay,pIndex[i].fHigh);
//* Entity of white
pMemDC->Rectangle(pIndex[i].iDay - iRectWidth,pIndex[i].fOpen,pIndex[i].iDay + iRectWidth,pIndex[i].fClose);
//* Bottom-shadow line.
pMemDC->MoveTo(pIndex[i].iDay,pIndex[i].fOpen);
pMemDC->LineTo(pIndex[i].iDay,pIndex[i].fLow);
//* Draw Volume.
pMemDC->SelectObject(&penDrakGray);
pMemDC->Rectangle(pIndex[i].iDay - iRectWidth,m_iHeight - iDateHeight,pIndex[i].iDay + iRectWidth,pIndex[i].iVolume);
pMemDC->SelectStockObject(BLACK_PEN);
}
else //* 阴线
{
//* Up-shadow line.
pMemDC->MoveTo(pIndex[i].iDay,pIndex[i].fOpen);
pMemDC->LineTo(pIndex[i].iDay,pIndex[i].fHigh);
//* Entity of black.
rect.SetRect(pIndex[i].iDay - iRectWidth,pIndex[i].fClose,pIndex[i].iDay + iRectWidth,pIndex[i].fOpen);
pMemDC->FillRect(rect,&brBlack);
//* Draw Vloume.
rect.SetRect(pIndex[i].iDay - iRectWidth,m_iHeight - iDateHeight,pIndex[i].iDay + iRectWidth,pIndex[i].iVolume);
pMemDC->FillRect(rect,&brBlack);
}
}
//* Save to File.And Create BITMAPFILEHEADER.
BITMAPFILEHEADER bmfh;
ZeroMemory(&bmfh,sizeof(BITMAPFILEHEADER));
*((char *)&bmfh.bfType) = 'B';
*(((char *)&bmfh.bfType) + 1) = 'M';
bmfh.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);
bmfh.bfSize = bmfh.bfOffBits + (m_iWidth * m_iHeight) * g_iPixel / 8;
TCHAR szBMPFileName[32];
int iBMPBytes = m_iWidth * m_iHeight * g_iPixel / 8;
strcpy(szBMPFileName,m_szFileName);
CFile file;
if(file.Open(szBMPFileName,CFile::modeWrite | CFile::modeCreate))
{
file.Write(&bmfh,sizeof(BITMAPFILEHEADER));
file.Write(&(lpbmih->bmiHeader),sizeof(BITMAPINFOHEADER));
file.Write(pBits,iBMPBytes);
file.Close();
}
pMemDC->DeleteDC();
delete pMemDC;
delete pBitMap;
delete lpbmih;
}
————————————————————————
有编程经验的程序员都知道:要使应用程序的界面美观不可避免的要使用大量位图。现在流行的可视化编程工具对位图的使用提供了很好的支持,被称为三大可视化开发工具的VB、VC、Delphi通过封装位图对象对位图使用提供了很好的支持:VB提供了两个功能很强的对象:PictureBox及Image,通过使用它们,装载、显示位图变得非常容易。Delphi中也提供了一个位图对象:TImage,它的功能与用法与VB中的Image类似。在VC中通过使用设备相关类CDC与GDI对象类CBitmap来完成位图的操作。
然而在VC中使用CBitmap类必须将BMP位图装入资源中,然后通过类 CBitmap的成员函数使用它,在通过CDC类的成员函数操作它。这样做有两点缺陷:将位图装入资源导致可执行文件增大,不利于软件发行;只能使用资源中有限的位图,无法选取其它位图。而且BMP位图文件是以DIB(设备无关位图)方式保存,BMP位图装入资源后被转换为DDB(设备相关位图),类CBitmap就是对一系列DDB操作的API函数进行了封装,使用起来有一定的局限性,不如DIB可以独立于平台特性。
要弥补使用资源位图的两点不足,就必须直接使用BMP位图文件。VC的示例中提供了一种方法读取并显示BMP位图文件,但使用起来相当的麻烦。首先使用API函数GlobalAlloc分配内存并创建HDIB位图句柄,所有操作只能直接读写内存,然后通过StrechDIBits及SetDIBsToDevice函数来显示于屏幕上,操作起来费时费力。
因此笔者通过研究类CBitmap的封装与DIB结构,使用Win32中提供的新函数,建立了一个专用于操作BMP文件的类,而且完全仿照类CBitmap的实现:从类CGdiObject派生,新类的所有接口与类CBitmap 的部分接口完全相同。这样对于习惯使用CBitmap类接口用法的程序员来说两者的接口在使用上没有什么分别。
首先我们先简单介绍一下DIB的结构。DIB位图既可以存在于内存,也可以以文件形式保存在磁盘上(BMP文件)。所有DIB都包含两部分信息:位图信息(BITMAPINFO),包括位图信息头和颜色表;位图数据。对于内存中DIB的只要有上述两部分就行,而对于DIB文件则还要加上位图文件头。
其次,Win32中提供了一个新函数CreateDIBSection,通过它可以创建一个存储DIB位的内存区域,既可以执行相应的GDI操作,又可以直接通过指向DIB位区域的指针方位DIB位区域。这是一个非常有用的函数,通过它我们可以用DIB替代DDB。
在了解了相应的知识后,我们可以自己由类CGdiObject派生一个操作BMP文件的类:CBitmapFile。
在自己编写类时有两点值得注意:
在BitmapFile.h文件中定义类CBitmapFile,首先必须声明类CBitmapFile是从类CGdiObject中公有派生。然后在类中首先使用宏DECLARE_DYNAMIC(CBitmapFile)表明新类的最高父类是类CObject,是符合MFC的类库规范。紧接着宏DECLARE_DYNAMIC的是声明静态函数FromHandle,这两个声明必须放在类定义的最前面。
在BitmapFile.cpp文件中类的成员函数的实现前加上IMPLEMENT_DYNAMIC(CBitmapFile,CGdiObject);表明类CBitmapFile直接派生于类CGdiObject。
在类CBitmapFile的声明中有三个函数与类Cbitmap中的定义稍有不同:
在类CbitmapFile中LoadBitmap函数的参数是LPCTSTR型,保存的是BMP文件的文件名。
在类CbitmapFile中CreateBitmap函数的参数中少了参数nPlanes,在函数内部默认为1。
在类CbitmapFile中CreateBitmapIndirect函数的参数中多了参数lpBits,它指向指定位图DIB位的内存区域。
在成员函数中最重要的是函数CreateBitmapIndirect和函数LoadBitmap:
在函数CreateBitmapIndirect中使用函数CreateDIBSection创建了一个以兼容DC为基础的HBITMAP句柄,并用继承自类CGdiObject 的函数Attach把它与类CGdiObject的句柄m_hObject关联起来。然后将指定位图的DIB位图数据拷贝到由函数CreateDIBSection创建的DIB位的内存区域。
在函数LoadBitmap中首先从指定文件名的文件中读取以结构BITMAPFILEHEADER为大小的数据块,然后由文件头标志判断文件是否为BMP位图文件,然后由BITMAPFILEHEADER中bfSize保存的文件大小与文件的真实大小比较文件是否有损坏,再由BITMAPFILEHEADER中bfOffBits与BITMAPFILEHEADER结构大小相减计算出位图信息头和颜色表一共的大小,动态申请一块空间保存位图信息头和颜色表信息,再由BITMAPFILEHEADER中bfSize与bfOffBits相减计算出DIB位图数据的大小,动态申请一块空间保存DIB位图数据,最后调用成员函数CreateBitmapIndirect来创建DIB位图。
在应用程序的OnPaint()事件中绘制DIB位图的方法与使用类CBitmap时绘制位图的方法完全相同,但有一点要注意的是由于CDC类没有提供返回新类CBitmapFile指针类型的将DIB位图选入内存的SelectObject函数,所以在使用SelectObject时要将返回类型强制转换为CbitmapFile *类型。
最后
以上就是闪闪百合最近收集整理的关于Visual C++中DDB与DIB位图编程全攻略的全部内容,更多相关Visual内容请搜索靠谱客的其他文章。
发表评论 取消回复