Windows GDI 教程(六) DC与加载位图

为了能够理清楚一些常见的 DC ,这里请让博主在介绍一下几种 DC 的获得方式:

  • BeginPaint 通过窗口 (hwnd) 获得
  • GetDC 通过窗口 (hwnd) 获得
  • CreatCompatibleDC 通过 hdc 获得

关于这三种情况获得的 DC,需要注意的是:从窗口上获得到的DC在释放资源的时候需要调用 ReleaseDC (释放引用,这样其他地方还可以接着用),而通过 CreatCompatibleDC 创建的 DC 我们通常称之为 Memory DC (博主管这叫“缓存DC”),当你不需要它的时候,需要通过 DeleteDC 来释放资源(因为这个DC是你单独申请的,如果你不用了,其他也不会有地方用它,只能直接 Delete)。

BeginPaint 与 GetDC

同样是通过窗口获得其客户端区域(Client area)的可绘制DC,BeginPaint 函数是先使窗口有效再重画,而 GetDC 没有这样,使用 GetDC 时由于窗口始终处于无效状态,不断会产生WM_PAINT消息。

BeginPaint 的表现是,收到 WM_PAINT 时绘制。所以正常情况下,如果没有手动触发 WM_PAINT 那么打开窗口则只绘制一次。

GetDC 的表现是,不断收到 WM_PAINT 消息,绘图区域(DC)不停地刷新。如果绘制过程比较慢的

CreatCompatibleDC

CreatCompatibleDC 函数的书面描述是,用来创建一个与指定设备兼容的内存设备上下文环境(DC)。

这个书面描述感觉有点不好理解,这里博主说一下自己的看法:

关于这个 CreatCompatibleDC 函数首先要了解这其中的单词:Creat、Compatible、DC。其中 Create 是“创建”、Compatible 是兼容的、DC 是博主说的“可画图的内存对象”。那么创建和 DC 都很好理解。这里我们重点要理解这个“兼容的”这个单词。

3个意思:

  1. 该DC的类型与兼容目标相同。虽然博主一再强调关于DC只需要把它当做“可以用来画图的内存就好了”,但是“橘生淮南则为橘,生于淮北则为枳”,从不同的设备中创建出来的DC,互相之间也是有差别的,由此产生的DC的类型包括:显示器、打印机、存储器和数据的索引。
  2. 该DC不需要指定区域、大小。感觉这一条有点凑数的感觉(博主:别打我!)
  3. 该DC与兼容的对象可以方便交流数据。该功能在GDI内部实现。比如使用 BitBlt 函数可以将兼容 DC 上的位图写入到目标 DC中。

加载BMP位图实例

BMP 是 Bitmap 的简写,中文常常称之为 “位图”。

#define WINDOW_WIDTH 400
#define WINDOW_HEIGHT 300

void Paint(HWND hwnd)
{
	PAINTSTRUCT ps;
	HDC hdc;
	HDC	mdc;
	HBITMAP hbmp; // 位图绘制对象句柄

	hdc = BeginPaint(hwnd, &ps); 

	// 创建缓存DC (当前窗口DC的兼容DC)
	mdc = CreateCompatibleDC(hdc);

	// 加载 "E:\bg.bmp" 到位图绘制对象 hbmp 中
	hbmp = (HBITMAP)LoadImage(
		NULL,			// 模块实例句柄(要加载的图片在其他DLL中时)
		"E:\bg.bmp",	// 位图路径
		IMAGE_BITMAP,	// 位图类型
		WINDOW_WIDTH,	// 指定图片宽
		WINDOW_HEIGHT,	// 指定图片高
		LR_LOADFROMFILE	// 从路径处加载图片
		);

	// 缓存DC选择位图绘制对象(可以理解为将图片存到mdc中)
	SelectObject(mdc, hbmp);

	// 将缓存DC中的位图复制到窗口DC上
	BitBlt(
		hdc,			// 目的DC
		0,0,			// 目的DC的 x,y 坐标
		WINDOW_WIDTH,	// 要粘贴的图片宽
		WINDOW_HEIGHT,	// 要粘贴的图片高
		mdc,			// 缓存DC
		0,0,			// 缓存DC的 x,y 坐标
		SRCCOPY 		// 粘贴方式
		);

	DeleteObject(hbmp);
	DeleteDC(mdc);
	EndPaint(hwnd, &ps);
}

效果图:
加载BMP图片

如何理解上述代码

对于初学者很容易对上述代码迷惑。常见的几个问题列举:

为什么要用 HBitMap 去 LoadImage,而不是直接用 DC 去 Load?

按照博主的思路来描述一下这个问题吧,画图可以简单的分成三个部分,分别是:
载体(如白纸、油画画板)、工具(如2B铅笔、马克笔)、颜料

对应到GDI程序中,使用画笔可以看成:载体(DC)、工具(画笔 HPEN)、颜料CreatePen 函数中传入)。

那么对应加载位图可以这样看:载体(DC)、工具(?)、颜料LoadImage 中指定图片)

这样很明显可以看到缺少一个工具加载位图的工具。也就是我们的 HBITMAP 对象。

这里拓展一下GDI中可以在DC用来绘制的工具:

对象 通过什么函数创建
Bitmap CreateBitmap, CreateBitmapIndirect, CreateCompatibleBitmap, CreateDIBitmap, CreateDIBSection
Bitmap对象只能被缓存DC(通过 CreatCompatibleDC创建)选中,并且单个bitmap不能同时被多个DC选中。
Brush CreateBrushIndirect, CreateDIBPatternBrush, CreateDIBPatternBrushPt, CreateHatchBrush, CreatePatternBrush, CreateSolidBrush
Font CreateFont, CreateFontIndirect
Pen CreatePen, CreatePenIndirect
Region CombineRgn, CreateEllipticRgn, CreateEllipticRgnIndirect, CreatePolygonRgn, CreateRectRgn, CreateRectRgnIndirect

此列表出处见 SelectObject function on MSDN

为什么要用 LoadImage 到 HBitMap,然后让 mdc 去 select,然后再让 mdc 复制到 hdc?

如果C语言基础学的好的话,那么这个问题也可以用一个很简单的角度去看待:
1.申请缓存空间
2.加载数据到缓存
3.输出数据(将缓存中的数据写入到目标内存)

以基础的C语言代码为例,假设是“输出用户输入的一个整数”,那么C语言代码如下:

// 1.(申请缓存空间) 声明一个4字节的int型变量,用于缓存用户输入
int num;
// 2.(加载数据到缓存) 将用户输入的数字加载到num中
scanf("%d", &num);
// 3.(将缓存中的数据写入到目标内存) 输出 num 到屏幕(将num的值写入到显存)
printf("你输入的是:%d n", num);

对应到加载图片的程序就可以这样理解了。

void Paint(HWND hwnd)
{
	PAINTSTRUCT ps;
	HDC hdc;
	HDC	mdc;
	HBITMAP hbmp;

	hdc = BeginPaint(hwnd, &ps); 

	// 1.(申请缓存空间) 创建缓存DC
	mdc = CreateCompatibleDC(hdc);

	// 2.(加载数据到缓存) 通过 Hbitmap 加载数据到 缓存DC
	hbmp = (HBITMAP)LoadImage( ... );
	SelectObject(mdc, hbmp);

	// 3.(将缓存中的数据写入到目标内存) 将缓存DC中的位图复制到窗口DC上
	BitBlt( ... );

	DeleteObject(hbmp);
	DeleteDC(mdc);
	EndPaint(hwnd, &ps);
}

这代码也就是看起来长了一点,没什么大不了的,本质上还是基础的程序逻辑。区别仅仅在于:
1.缓存的空间从 int 换成了 HDC
2.加载数据的方法从使用 scanf 换成了 LoadImageSelectObject
3.输出数据的对象从 int类型的num 写入到显存,换成了 HDC类型的mdc 写入到 hdc;输出数据的方法从 printf 换成了 BitBlt

PS:引用博主一句 装逼 的话来总结一下上面的对比:“程序做并且只在做两件事,一、读数据,二、写数据”

为什么不直接让 hdc 直接去 select 图片的 HBitMap?

1.首先上文有提到过 Bitmap 对象只能被缓存DC (Memory DC,通过 CreateCompatibleDC 创建的)选中。

2.其次,就算情况1不存在,bitmap 可以被 hdc 选中,这在程序的逻辑上也是一件很别扭的事情。依旧是套用 C语言基础的程序来看这个问题:

// 1.不申请缓存空间

// 2.3.直接获取数据并输出
printf("你输入的是:%d n", scanf( ? ) );

或者这样:

// 1.不申请缓存空间

// 2.3.直接获取数据并输出
printf("你输入的是:%s n", gets( ? ) );

基础好的可以马上发现上面两个示意的代码都是没法实现的。当然,如果你依旧觉得这个合理也是可以的,毕竟在其他的语言(比如python)里面这种情况是可以实现的。但是这样写的话,会有如下两种情况:

  1. 不自由。无法修改该数据,只能死板的输出。
  2. 效率低。没有缓存,要输出的时候必须要当时加载,下一次再次用到的时候又要重新加载。

综上。缓存DC是有存在的必要。

修改图片之后再显现的实例

为了照应上文,这里附上一个修改缓存DC中图片然后再显示到窗口的例子。

void Paint(HWND hwnd)
{
	PAINTSTRUCT ps;
	HDC hdc;
	HDC	mdc;
	HBITMAP hbmp; // 位图绘制对象句柄

	hdc = BeginPaint(hwnd, &ps); 

	// 加载图片到缓存 DC
	mdc = CreateCompatibleDC(hdc);
	hbmp = (HBITMAP)LoadImage(NULL, "E:\bg.bmp", IMAGE_BITMAP, WINDOW_WIDTH, WINDOW_HEIGHT, LR_LOADFROMFILE);
	SelectObject(mdc, hbmp);

	// 修改缓存DC (在缓存的图片上写字)
	TextOut(mdc, 0,0, TEXT("Hi, 缓存DC你好。"), strlen(TEXT("Hi, 缓存DC你好。")));

	// 将缓存DC中的位图复制到窗口DC上
	BitBlt(hdc, 0,0, WINDOW_WIDTH, WINDOW_HEIGHT, mdc, 0,0, SRCCOPY);

	DeleteObject(hbmp);
	DeleteDC(mdc);
	EndPaint(hwnd, &ps); 
}

效果图:
修改缓存DC

教程索引

留下评论