18.2 增强型图元文件

Posted wiljm

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了18.2 增强型图元文件相关的知识,希望对你有一定的参考价值。

摘录于《Windows程序(第5版,珍藏版).CHarles.Petzold 著》P853

        “增强型图元文件”格式是在 32 位的 Windows 中才引入的。它涉及一系列新的函数、几个新的数据结构、新的数据结构、新的剪贴板格式和新的文件扩展名 .EMF。

        最重要的改进是新的图元文件格式包含了可以通过函数调用获得的更广泛的头信息。这些信息的目的是帮助应用程序显示图元文件图像。

        还有一些增强型图元文件的函数允许你在增强型格式(EMF)和旧格式之间来回转换,后者又被称为 Windows 图元文件格式(WMF)。当然,这种转换有可能不太顺利,因为老式的图元文件格式不支持某些新的 32 位图形特性,比如路径等。

18.2.1  基本步骤

        图 18-3 所示的程序 EMF1 创建并显示一个增强型图元文件,显示出的图像会有一小点变形。

/*------------------------------------------------
	EMF1.C -- Enhanced Metafile Demo #1
		  (c) Charles Petzold, 1998
------------------------------------------------*/

#include <Windows.h>

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
	PSTR szCmdLine, int iCmdShow)

	static TCHAR szAppName[] = TEXT("EMF1");
	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("Enhanced Metafile Demo #1"),
		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 HENHMETAFILE	hemf;
	HDC					hdc, hdcEMF;
	PAINTSTRUCT			ps;
	RECT				rect;


	switch (message)
	
	case WM_CREATE:
		hdcEMF = CreateEnhMetaFile(NULL, NULL, NULL, NULL);

		Rectangle(hdcEMF, 100, 100, 200, 200);

		MoveToEx(hdcEMF, 100, 100, NULL);
		LineTo(hdcEMF, 200, 200);

		MoveToEx(hdcEMF, 200, 100, NULL);
		LineTo(hdcEMF, 100, 200);

		hemf = CloseEnhMetaFile(hdcEMF);

		return 0;

	case WM_PAINT:
		hdc = BeginPaint(hwnd, &ps);

		GetClientRect(hwnd, &rect);

		rect.left = rect.right / 4;
		rect.right = 3 * rect.right / 4;
		rect.top = rect.bottom / 4;
		rect.bottom = 3 * rect.bottom / 4;

		PlayEnhMetaFile(hdc, hemf, &rect);
		
		EndPaint(hwnd, &ps);
		return 0;

	case WM_DESTROY:
		DeleteEnhMetaFile(hemf);
		PostQuitMessage(0);
		return 0;
	
	return DefWindowProc(hwnd, message, wParam, lParam);

        EMF1 的窗口过程在处理 WM_CREATE 消息的过程中,从调用 CreateEnhMetaFile 函数开始,创建增强型图元文件。该函数需要四个参数,但可以都设置为 NULL。我稍后会讨论如何使用非 NULL 值的参数。

        就像 CreateMetaFile 那样,CreateEnhMetaFile 函数会返回一个特殊的设备环境的句柄。程序用这个句柄绘制一个矩形和两条对角线。这些函数调用和它们的参数被转换为二进制形式并存储在图元文件中。

        最后,一个 CloseEnhMetaFile 调用结束了这个增强型图元文件的创建工作,并返回一个指向它的句柄。该句柄存在一个 HENHMETAFILE 类型的静态变量中。

        在处理 WM_PAINT 消息的过程中,EMF1 通过一个 RECT 结构获得程序客户区窗口的尺寸。(程序)调整该结构的四个字段,使矩形的宽度和高度是客户区窗口的一半,并将其放在后者的中央。之后,EMF1 调用 PlayEnhMetaFile 函数。它的第一个参数是窗口的设备环境句柄,第二个参数是增强型图元文件的句柄,第三个参数是指向 RECT 结构的指针

        在该图文件的创建过程中,GDI 会计算出整个图元文件图像的尺寸。在本例中,图像的高和宽各是 100 个单位。当现实图元文件的时候,GDI 伸展图像以适应 PlayEnhMetaFile 函数指定的矩形范围。EMF1 程序在 Windows 下运行的三个实例如图 18-4 所示。

        最后,在处理 WM_DESTROY 消息期间,EMF1 通过调用 DeleteEnhMetaFile 删除该图元文件。

        让我们来总结一下从程序 EMF1 学到的东西。

        首先,在这个示例 程序中,在创建图元文件时,绘制矩形和画线函数中使用的坐标其实并不重要。你可以同时给它们加倍或同时减去一个常数,结果是一样的。在定义一个图像时,最重要的是坐标之间对应的关系。

        第二,图像会被拉伸,以满足传递给 PlayEnhMetaFile 函数的矩形的尺寸限制。因此,正如图 18-4 所清楚地显示的,图像可能会变形。虽然图元文件的坐标表示该图像时一个正方形,但一般情况下我们看到的度不是这样。有时,这正是想要的效果。比如在把图像嵌入到字处理文本中时,可能会让用户为图像指定一个矩形区域,程序要保证整个图像嵌入到字处理文本中时,可能会让用户为图像指定一个矩形区域,程序要保证整个图像完全嵌入该区域而不浪费空间。用户可以通过适当地调整该矩形来获得正确的纵横比。

        但是,有时候这是不合适的。你可能需要保持原始图像的纵横比,因为它对呈现视觉信息非常重要。例如,警方使用的犯罪嫌疑人素描既不应比原图胖也不应比原图瘦。或者,你可能希望保留原始图像的尺寸大小。在某些情况下,图像是两英寸高这点很重要,否则很难被复制。

        此外请注意,在该图元文件中画的线似乎没有完全连到矩形的角上。这是由于 Windows 在图元文件中存储矩形坐标的方式造成的。本章稍后我们将解决这个问题。

图 18-4  EMF1 程序的显示

18.2.2  窥探内部机制

        看一下图元文件的内容,就会对图元文件有一个很好的了解。如果有一个可以查看的基于磁盘的图元文件,这件事就非常简单了。因此,图 18-5 所示的 EMF2 程序就创建了这样一个文件。

/*------------------------------------------------
	EMF2.C -- Enhanced Metafile Demo #2
		  (c) Charles Petzold, 1998
------------------------------------------------*/

#include <Windows.h>

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
	PSTR szCmdLine, int iCmdShow)

	static TCHAR szAppName[] = TEXT("EMF2");
	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("Enhanced Metafile Demo #2"),
		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)


	HDC					hdc, hdcEMF;
	HENHMETAFILE		hemf;
	PAINTSTRUCT			ps;
	RECT				rect;


	switch (message)
	
	case WM_CREATE:
		hdcEMF = CreateEnhMetaFile(NULL, TEXT("emf2.emf"), NULL,
			TEXT("EMF2\\0EMF Demo #2\\0"));

		if (!hdcEMF)
			return 0;

		Rectangle(hdcEMF, 100, 100, 200, 200);

		MoveToEx(hdcEMF, 100, 100, NULL);
		LineTo(hdcEMF, 200, 200);

		MoveToEx(hdcEMF, 200, 100, NULL);
		LineTo(hdcEMF, 100, 200);

		hemf = CloseEnhMetaFile(hdcEMF);

		DeleteEnhMetaFile(hemf);
		return 0;

	case WM_PAINT:
		hdc = BeginPaint(hwnd, &ps);

		GetClientRect(hwnd, &rect);

		rect.left = rect.right / 4;
		rect.right = 3 * rect.right / 4;
		rect.top = rect.bottom / 4;
		rect.bottom = 3 * rect.bottom / 4;

		if (hemf = GetEnhMetaFile(TEXT("emf2.emf")))
		
			PlayEnhMetaFile(hdc, hemf, &rect);
			DeleteEnhMetaFile(hemf);
		

		EndPaint(hwnd, &ps);
		return 0;

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	
	return DefWindowProc(hwnd, message, wParam, lParam);
a

        在 EMF1 程序中,CreateEnhMetaFile 函数的所有参数都设置为 NULL。在 EMF2 程序中,第一个参数也设置为 NULL。此参数可以是一个设备环境的句柄。我们很快就会讨论到,GDI 用该参数在图元文件的头中插入度量信息如果该参数设置为 NULL,GDI 就假定度量信息时基于视频设备环境的

        CreateEnhMetaFile 的第二个参数是文件名。如果将此参数设置为 NULL(EMF1 就是如此,但 EMF2 不是),该函数将创建一个内存图元文件。EMF2 程序会创建一个基于磁盘的图元文件,其文件名为 EMF2.EMF。

        该函数的第三个参数是 RECT 结构的地址,该结构以 0.01 mm 为单位指定图元文件的总体尺寸。很快就能看到,在图元文件的头中加入这项信息时非常重要的(这恰恰是旧的 Windows 图元文件格式的不足之一)。如果将这个参数设置为 NULL,那么 GDI 会帮你把尺寸计算出来。我喜欢让操作系统来做这些事情,所以我将这个参数设置为 NULL。如果性能对应用程序来说极为关键,就可能要使用该参数,以避免 GDI 做额外的工作

        最后一个参数是描述图元文件的文本字符串。该字符串分为两段:第一段是以 NULL 字符结尾的应用程序的名称(不一定要和程序的文件名相同),第二段是以两个 NULL 字符结尾的图像描述。例如,如果用 C 语言的 ‘\\0' 表示 NULL 字符,那么描述字符串可以为“LoonyCad V6.4\\0Flying Frogs\\0\\0”。由于 C 语言通常会在带引号的字符串的末尾加上一个 NULL 字符,所以你只需要在结尾再放一个 '\\0',如 EMF2 所示。

        在创建图元文件之后,和 EMF1 一样,EMF2 程序使用从 CreateEnhMetaFile 函数返回的设备环境的句柄来调用一些 GDI 函数。然后,程序调用 CloseEnhMetaFile 函数删除该设备环境句柄,并获得最终的图元文件的句柄。

        然后,还是在处理 WM_CREATE 期间,EMF2 做了一些 EMF1 没有做的事情:就在得到图元文件的句柄后,程序调用了 DeleteEnhMetaFile 函数。该函数会释放维护图元文件所需的所有内存资源。但是基于磁盘的图元文件仍然存在。(要想删除该文件,可以调用普通的文件删除函数如 DeleteFile。)注意,和 EMF1 的做法不同,这里的图元文件句柄不是一个静态变量,这意味着一条消息处理结束后没有必要保存它的值以供下一条消息使用。

        现在,EMF2 需要进行磁盘文件访问来使用该图元文件。这是通过在 WM_PAINT 消息的处理过程中调用 GetEnhMetaFile 函数来实现的。此函数的唯一参数就是磁盘图元文件的文件名。该函数返回图元文件的句柄。如同 EMF1 一样,EMF2 把该句柄传递给 PlayEnhMetaFile 函数。PlayEnhMetaFile 函数的最后一个参数定义了一个矩形,程序将在该矩形中显示图元文件的图像。但与 EMF1 不同的是,EMF2 在 WM_PAINT 消息处理结束之前删除了该图元文件。以后每次处理 WM_PAINT 消息时,EMF2 都会再重新读取图元文件、显示然后删除它。

        请记住删除图元文件只是删除去了存储它所需要的内存资源。即使在程序执行结束后,基于磁盘的图元文件仍然存在。

        因为 EMF2 会留下一个基于磁盘的图元文件,所以你可以查看它的内容。图 18-6 以十六进制方式显示了该程序创建的 EMF2.EMF 文件的内容。


图 18-6  EMF2.EMF 文件的十六进制转储

        我需要说明一下,图 18-6 显示的图元文件是 EMF2 在 Windows NT4 上,使用 1024 * 768 的显示分辨率时创建的。稍后将讨论,如果同一程序运行在 Windows 98 上,那么创建的图元文件会少 12 个字节(译者注:如果该程序运行在 Windows Vista 或 Windows 7 平台上,那么长度要比 NT 4 平台上多 8 个字节,这是由于图元文件头新添加了一个 SIZEL 类型的字段。)。此外,显示分辨率也会影响图元文件头的部分信息。

        查看增强型图元文件格式能使我们更深入地了解图元文件的运作机制。增强型图元文件包含一些长度可变的记录。在头文件 WINGDI.H 中,这些记录的一般格式由 ENHMETARECORD 结构来描述,如下所示:

typedef struct tagENHMETARECORD

    DWORD iType;        // 记录的类型
    DWORD nSize;        // 记录的大小
    DWORD dParm[1];     // 存放参数的数组

ENHMETARECORD;
当然,那个只有一个元素的数组实际表示的是该数组的元素数目是不定的,具体元素的数目取决于记录的类型。iType 字段可以是 WINGDI.H 文件中定义的近 100 个以前缀 EMR_ 开头的常量之一。 nSize 字段定义整个记录的长度,包括 iType 字段、nSize 字段和一个或多个 dParm 字段

        有了这些知识,让我们来看图 18-6。第一个记录的类型为 0x00000001,长度为 0x00000088,所以它占用该文件的前 136 个字节。记录的类型为 1,对应常量为 EMR_HEADER。我将把对文件头(header)的讨论留到后面,所以现在让我们跳到偏移量 0x0088(就是这第一个记录的末尾)。

        接下来的五个记录对应于在创建图元文件后 EMF2 程序的五个 GDI 调用。位于偏移量 0x0088 位置的记录的类型是 0x0000002B,也就是常量 EMR_RECTANGLE,显示这是调用 Rectangle 函数的图元文件记录。它有 0x00000018(十进制的 24)字节长,可以容纳四个 32 位的参数。Rectangle 函数实际上有五个参数,但当然,第一个参数(设备环境的句柄)因为没有实际意义所以不存储在该图元文件中。虽然在 EMF2 中调用 Rectangle 函数时指定的顶角是(100, 100)和(200, 200),但是图元文件中的四个参数有两个为 0x00000063(或 99),另外两个为 0x000000C6(或 198)。在 Windows 98 下,EMF2 程序创建的图元文件将显示两个 0x00000064(或 100)参数和两个 0x000000C7(或 199)参数。显然,Windows 在把 Rectangle 函数的参数存储至图元文件之前对它们作出了调整,但这种调整不是很一致。这就是为什么矩形的对角线不连接四个顶点的原因。

        接下来,我们有四个 16 字节的记录分别对应于两个 MoveToEx(0x0000001B 或 EMR_MOVETOEX)函数调用和两个 LineTo(0x00000036 或 EMR_LINETO)函数调用。在该图元文件中保存的参数值和传递给那些函数的实际参数值相同。

        该图元文件以 0x0000000E 类型(EMR_EOF,即“文件结尾”)结尾,这是一个长度为 20 字节的记录。

        增强型图元文件总是以头记录开始,它对应于 ENHMETAHEADER 类型的结构,定义如下:

typedef struct tagENHMETAHEADER

    DWORD iType;           // EMR_HEADER = 1
    DWORD nSize;           // 结构的大小
    RECTL rclBounds;       // 以像素为单位的矩形边框
    RECTL rclFrame;        // 以 0.01mm 为单位的图像尺寸
    DWORD dSignature;      // ENHMETA_SIGNATURE = " EMF"
    DWORD nVersion;        // 0x00010000
    DWORD nBytes;          // 以字节为单位的文件长度
    DWORD nRecords;        // 文件含有的记录数
    WORD nHandles;        // 句柄表中的句柄数
    WORD sReserved;       
    DWORD nDescription;    // 描述字符串的字符长度
    DWORD offDescription;  // 描述字符串在文件中的起始偏移位置
    DWORD nPalEntries;     // 调色板中条目数
    SIZEL szlDevice;       // 以像素为单位的设备分辨率
    SIZEL szlMillimeters;  // 以 mm 为单位的设备分辨率
    DWORD cbPixelFormat;   // 像素格式的尺寸
    DWORD offPixelFormat;  // 像素格式的其实偏移位置
    DWORD bOpenGL;         // 在不含 OpenGL 记录时,该值为 FALSE

ENHMETAHEADER;

        这个头记录的加入可能是增强型图元文件格式对老格式进行的最大一项改进。不需要对基于磁盘的图元文件使用文件输入/输出函数就能获取这些头信息。如果有某个图元文件的句柄,可以如下调用 GetEnhMetaFileHeader 函数:

GetEnhMetaFileHeader (hemf, cbSize, &emh);
第一个参数是图元文件句柄,最后一个参数是指向 ENHMETAHEADER 结构的指针,第二个参数是此结构的大小。 使用类似的 GetEnhMetaFileDescription 函数,可以获得描述字符串

        如上面定义的,ENHMETAHEADER 结构的长度为 100 个字节,但在 EMF2.EMF 图元文件中,由于该记录还包括一个描述字符串,所以记录大小是 0x88(136 字节)。存储在 Windows 98 图元文件中的头记录并不包括 ENHMETAHEADER 结构的最后三个字段,这造成了在长度上有 12 个字节的区别。

        rclBounds 字段是一个 RECT 结构,它表示该图像的尺寸(以像素为单位)。如果把它从十六进制翻译成十进制,可以看到图像边界以点(200, 200)为右下角,并以点(100, 100)为左上角,跟我们预期的完全一致。

        rclFrame 字段是另一个矩形结构,它提供相同的信息,但以 0.01mm 为单位。在这里,文件显示了以点(0x0C35, 0x0C35)和点(0x186A,0x186A)定义的矩形边框,用十进制表示的话,这两个点是(3125, 3125)和(6250, 6250)。这些信息时从哪里来的呢?稍后我们将揭晓答案。

        dSingature 字段总是被设置为 ENHMETA_SIGNATURE(即 0x464D4520)。这个数字看起来很奇怪,但如果反向排列它的字节(按照 Intel 处理器在内存中存储多字节值的方式),并把其转换为 ASCII 字符,它就是简单的“ EMF”字符串。dVersion 字段的值总是 0x00010000。

        紧接着是 nBytes 字段,在这里是 0x000000F4,即图元文件的总字节数。nRecords 字段(在这里是 0x00000007)表示记录的总数——一个头记录、五个 GDI 函数调用和一个文件结束记录。

        接下来有两个 16 位字段。nHandles 字段值为 0x0001。通常,此字段表示在图元文件中使用的图形对象(如画笔、画刷、字体)的非默认句柄的数目。我们还没有进行这项工作,因此你可能会认为这个字段值为 0,但 GDI 为自己保留了第一个。很快我们就会看到句柄是如何存储在图元文件中的。

        下面的两个字段指定了描述字符串的长度(字符的个数)及它在文件中的偏移量,在这里分别是 0x00000012(十进制的 18)和 0x00000064。如果图元文件没有描述字符串,那么这两个字段为 0。

        nPalEntries 字段表示图元文件的调色板中的条目的个数,在这里为 0。

        这个头记录接着还包含两个 SIZEL 结构,它们含有两个 32 位的字段,分别是 cx 和 cy。szlDevice 字段(在图元文件的偏移量 0x0040 位置)表示输出设备的尺寸,以像素为单位。szlMillimeters 字段(在图元文件的偏移量 0x0050 位置)则以 mm 为单位表示输出设备的尺寸。在增强型图元文件的技术文档中,该输出设备被称为“参考设备”(reference device)。此设备的句柄对应于 CreateEnhMetaFile 函数调用中的第一个参数所指定的设备环境句柄。如果将该参数设置为 NULL,GDI 就是用视频显示。在 EMF2 创建前面所示的图元文件时,我恰好是用的是 Windows NT 上的 1024 * 768 的视频模式,因此那就是 GDI 所用的参考设备。

        GDI 通过调用 GetDeviceCaps 函数获取这些信息。EMF2.EMF 的 szlDevice 字段的值是 0x0300 * 0x0400(也就是 1024 * 768),这是用 HORZRES 和 VERTRES 参数从 GetDeviceCaps 获得的。szlMillimeters 字段的值是 0x140 * 0xF0(320 * 240),是使用 HORZSIZE 和 VERTSIZE 参数从 GetDeviceCaps 获得的。

        使用简单的除法就可以知道像素的高和宽都为 0.3125 mm,这就是 GDI 计算前面所说的 rclFrame 矩形尺寸的方法。

        在该图元文件中,ENHMETAHEADER 结构后面紧跟着描述字符串,这是 CreateEnhMetaFile 的最后一个参数。在此例中,这是字符串 “EMF2”后面跟一个 NULL 字符和“EMF Demo #2”后跟两个 NULL 字符。一共有 18 个字符,或者说是 36 个字节(因为它以 Unicode 形式存储)。无论创建该图元文件的程序运行在 Windows NT 还是在 Windows 98 上,这个字符串都使用 Unicode 存储

18.2.3  图元文件和 GDI 对象

        我们已经了解了图元文件中是如何存储 GDI 绘图命令的。现在来研究一下 GDI 对象是如何存储的。图 18-7 所示的 EMF3 程序类似于先前的 EMF2 程序,不同的是它创建了一个用来绘制矩形和直线的非默认的画笔和画刷。我们还以为 Rectangle 函数调用时发生的坐标问题提供了一个小小的修复方法。EMF3 调用 GetVersion 函数来确定它是运行在 Windows 98 上还是运行在 Windows NT 上,然后相应地调增参数。

/*------------------------------------------------
	EMF3.C -- Enhanced Metafile Demo #3
		  (c) Charles Petzold, 1998
------------------------------------------------*/

#include <Windows.h>

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
	PSTR szCmdLine, int iCmdShow)

	static TCHAR szAppName[] = TEXT("EMF3");
	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("Enhanced Metafile Demo #3"),
		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)

	LOGBRUSH			lb;
	HDC					hdc, hdcEMF;
	HENHMETAFILE		hemf;
	PAINTSTRUCT			ps;
	RECT				rect;


	switch (message)
	
	case WM_CREATE:
		hdcEMF = CreateEnhMetaFile(NULL, TEXT("emf3.emf"), NULL,
			TEXT("EMF3\\0EMF Demo #3\\0"));

		SelectObject(hdcEMF, CreateSolidBrush(RGB(0, 0, 255)));

		lb.lbStyle = BS_SOLID;
		lb.lbColor = RGB(255, 0, 0);
		lb.lbHatch = 0;

		SelectObject(hdcEMF, 
			ExtCreatePen(PS_SOLID | PS_GEOMETRIC, 5, &lb, 0, NULL));

		if (GetVersion() & 0x80000000)			// Windows 98
			Rectangle(hdcEMF, 100, 100, 201, 201);
		else
			Rectangle(hdcEMF, 101, 101, 202, 202);

		MoveToEx(hdcEMF, 100, 100, NULL);
		LineTo(hdcEMF, 200, 200);

		MoveToEx(hdcEMF, 200, 100, NULL);
		LineTo(hdcEMF, 100, 200);

		DeleteObject(SelectObject(hdcEMF, GetStockObject(BLACK_PEN)));
		DeleteObject(SelectObject(hdcEMF, GetStockObject(WHITE_BRUSH)));

		hemf = CloseEnhMetaFile(hdcEMF);

		DeleteEnhMetaFile(hemf);
		return 0;

	case WM_PAINT:
		hdc = BeginPaint(hwnd, &ps);

		GetClientRect(hwnd, &rect);

		rect.left = rect.right / 4;
		rect.right = 3 * rect.right / 4;
		rect.top = rect.bottom / 4;
		rect.bottom = 3 * rect.bottom / 4;

		hemf = GetEnhMetaFile(TEXT("emf3.emf"));
		
		PlayEnhMetaFile(hdc, hemf, &rect);
		DeleteEnhMetaFile(hemf);
		
		EndPaint(hwnd, &ps);
		return 0;

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	
	return DefWindowProc(hwnd, message, wParam, lParam);

        如程序所示,当使用从 CreateEnhMetaFile 函数返回的设备环境句柄调用 GDI 函数的时候,函数调用被存储在图元文件中,而不是被送到屏幕或打印机。但是,一些 GDI 函数并不涉及某一特定的设备环境。这些 GDI 函数中,很重要的一类就是那些创建图形对象(包括画笔和画刷)的函数。虽然逻辑画笔和画刷的定义式存储在由 GDI 维护的内存中,但是这些抽象的定义并不与创建它们的任何特定的设备环境相关联。

        EMF3 调用了 CreateSolidBrush 和 ExtCreatePen 函数。这两个函数不需要使用设备环境句柄,也就意味着 GDI 不会把这些调用存储在图元文件里。这种推断是正确的。在它们被调用时,GDI 函数仅仅是创建图形绘图对象,而不会影响该图元文件。

        然而,在程序调用 SelectObject 来把 GDI 对象选入图元文件设备环境时,GDI 会把对象创建函数(实际上它是从用于存储该对象的内部 GDI 数据派生出来的和 SelectObject 调用都编码到图元文件中。为了解这种工作方式,让我们来看看 EMF3.EMF 的十六进制显示,如图 18-8 所示。

图 18-8  EMF3.EMF 的十六进制转储

        把这个图元文件与前面所示的 EMF2.EMF 比较一下。第一个区别是在 EMF3.EMF 文件头部分的 rclBounds 字段。EMF2.EMF 指定图像的边界在坐标(0x64, 0x64)和(0xC8, 0xC8)的区域内。在 EMF3.EMF 中,这个区域是(0x60, 0x60)和(0xCC, 0xCC)。这是因为后者使用了较粗的画笔。另外,rclFrame 字段(表示图像的尺寸,以 0.01mm 为单位)也受到了影响。

        EMF2.EMF 的 nBytes 字段(位于偏移量 0x0030)指出该图元文件的长度是 0xFA 字节,而在 EMF3.EMF 中,它是 0x0188 字节。EMF2.EMF 图元文件包含 7 个记录(一个头记录、五个 GDI 函数调用记录和一个文件结束记录),而 EMF3.EMF 有 15 个记录。我们将会看到,这额外的 8 个记录是:2 个对象创建函数、4 个 SelectObject 调用和 2 个 DeleteObject 函数的调用记录。

        nHandles 字段(位于文件中的偏移量 0x0038)指出了 GDI 对象的句柄的数目。该字段的值总是比图元文件所使用的非默认对象的数目多一个。(Platform SDK 文档是这么说的“表中索引为零的项目是保留项。”)该字段的值在 EMF2.EMF 中为 1,而在 EMF3.EMF 中为 3(指定了画笔和画刷)。

        让我们跳到文件中偏移量为 0x0088 的位置,它是第二个记录(文件头记录之后的第一个记录)的开始位置。该记录的类型为 0x27,对应于常数 EMR_CREATEBRUSHINDIRECT。这是 CreateBrushIndirect 函数的图元文件记录,它需要一个指向 LOGBRUSH 结构的指针。该记录的大小是 0x18(十进制的 24)个字节。

        每个被选入图元文件设备环境的非备用 GDI 对象都会被赋予一个数字编号,该编号从 1 开始。在这个记录里,接下来的 4 个字节指定了这个编号,具体位置位于图元文件偏移量 0x0090。再接下来的三个 4 字节字段分别对应于 LOGBRUSH 结构的三个字段:0x00000000(BS_SOLID 的 lbStyle 字段)、0x00FF0000(lbColor 字段)和 0x00000000(lbHatch)。

        EMF3.EMF 的下一个记录位于偏移量 0x00A0,记录类型是 0x25,对应 EMR_SELECTOBJECT,是 SelectObject 调用的图元文件记录。该记录的长度是 0x0C(12)字节。下一个字段是数字 0x01,该值表示函数选择第一个 GDI 对象,就是逻辑画刷。

        EMF3.EMF 的下一个记录位于偏移量 0x00AC,记录类型是 0x5F,对应 EMR_EXTCREATEPEN。该记录长度为 0x34(52)字节。接下来的 4 字节字段是 0x02,意思是这是该图元文件中使用的第二个非备用 GDI 对象。

        我也不知道为什么 EMR_EXTCREATEPEN 记录接下来的四个字段里将记录大小重复了两次,而且两个记录大小之间还用值为 0 的字段隔开了,但它们确实就这样:0x34、0x00、0x34 和 0x00。下一个字段是 0x00010000,它是 PS_SOLID(0x00000000)与 PS_GEOMETRIC(0x00010000)组合的画笔样式。下一个字段是五个单位的宽度,紧跟其后的是 ExtCreatePen 函数所使用的逻辑画刷结构的三个字段,以及一个值为 0 的字段。

        如果创建自定义的扩展画笔样式,EMR_EXTCREATEPEN 记录会超过 52 个字节,并且这将会反映在记录的第二个字段和两个重复的记录大小中。在描述 LOGBRUSH 结构的三个字段后面,紧接着的字段的值不会为 0(像 EMF3.EMF 那样),而是会指出短划线和空格的数量。其后是许多用于短划线和空格长度的字段。

        在 EMF3.EMF 中,接下来的 12 字节的字段是另一个 SelectObject 调用,它指定了第二个对象——画笔。接下来的五个记录跟 EMF2.EMF 的相同——一个类型为 0x2B(EMR_RECTANGLE)的记录和两组类型为 0x1B(EMR_MOVETOEX)与 0x36(EMR_LINETO)的记录。

        紧跟着这些绘图函数的是两组 12 字节长的类型为 0x25(EMR_SELECTOBJECT)和 0x28(EMR_DELETEOBJECT)的记录。两个选择对象的记录分别使用参数 0x80000007 和 0x80000000。当参数的最高位为 1 时,表示这是一个备用对象,在此例中是 0x07(对应于 BLACK_PEN)和 0x00(WHITE_BRUSH)。

        对于图元文件中的两个非默认对象,DeleteObject 调用使用的参数是 2 和 1。虽然 DeleteObject 函数不需要设备环境句柄作为第一个参数,但 GDI 显然跟踪记录了图元文件中被程序删除的对象。

        最后,该图元文件以一个 0x0E 记录结束,它就是 EMF_EOF(文件结束)。

        总结一下,每当有一个非默认的 GDI 对象被首次选入图元文件设备环境时,GDI 都会把创建对象的函数编码为一条记录(在此例中是 EMR_CREATEBRUSHINDIRECT 和 EMR_EXTCREATEPEN)。每个对象会有一个唯一的编号,从 1 开始,它由记录的第三个字段表示。跟在这个记录后面的是引用该编号的一条 EMR_SELECTOBJECT 记录。之后,只需要有一条 EMR_SELECTOBJECT 记录,就可以把一个对象选入图元文件设备环境了(如果此时该对象还没有被删除的话)。

18.2.4  图元文件和位图

        现在让我们尝试处理一些稍微复杂的情况,具体来说,就是在一个图元文件设备环境里画一个位图。图 18-9 所示的 EMF4 展示了这一过程。

/*------------------------------------------------
	EMF4.C -- Enhanced Metafile Demo #4
		  (c) Charles Petzold, 1998
------------------------------------------------*/

#define OEMRESOURCE
#include <Windows.h>

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
	PSTR szCmdLine, int iCmdShow)

	static TCHAR szAppName[] = TEXT("EMF4");
	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("Enhanced Metafile Demo #4"),
		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)

	BITMAP				bm;
	HBITMAP				hbm;
	HDC					hdc, hdcEMF, hdcMem;
	HENHMETAFILE		hemf;
	PAINTSTRUCT			ps;
	RECT				rect;

	switch (message)
	
	case WM_CREATE:
		hdcEMF = CreateEnhMetaFile(NULL, TEXT("emf4.emf"), NULL,
									TEXT("EMF4\\0EMF Demo #4\\0"));

		hbm = LoadBitmap(NULL, MAKEINTRESOURCE(OBM_CLOSE));

		GetObject(hbm, sizeof(BITMAP), &bm);

		hdcMem = CreateCompatibleDC(hdcEMF);

		SelectObject(hdcMem, hbm);

		StretchBlt(hdcEMF, 100, 100, 100, 100,
					hdcMem, 0, 0, bm.bmWidth, bm.bmHeight, SRCCOPY);

		DeleteDC(hdcMem);
		DeleteObject(hbm);

		hemf = CloseEnhMetaFile(hdcEMF);

		DeleteEnhMetaFile(hemf);
		return 0;

	case WM_PAINT:
		hdc = BeginPaint(hwnd, &ps);

		GetClientRect(hwnd, &rect);

		rect.left = rect.right / 4;
		rect.right = 3 * rect.right / 4;
		rect.top = rect.bottom / 4;
		rect.bottom = 3 * rect.bottom / 4;

		hemf = GetEnhMetaFile(TEXT("emf4.emf"));

		PlayEnhMetaFile(hdc, hemf, &rect);
		DeleteEnhMetaFile(hemf);

		EndPaint(hwnd, &ps);
		return 0;

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	
	return DefWindowProc(hwnd, message, wParam, lParam);

        为了方便起见,EMF4 加载的是一个由常量 OEM_CLOSE 指定的系统位图。在设备环境中显示一个位图的惯用方法是通过调用 CreateCompatibleDC 函数创建一个跟目标设备环境(在这里是图元文件的设备环境)兼容的内存设备环境。然后,通过调用 SelectObject 将位图选入内存设备环境,并调用 BitBlt 或 StretchBlt 从内存源设备环境传到目标设备环境。完成后,再删除内存设备环境和位图。

        可以注意到,EMF4 还调用了 GetObject 来确定该位图的大小。这是调用 SelectObject 前必须做的。

        第一眼看上去,想要把这段代码存储到图元文件中对 GDI 来说似乎是一个很大的挑战。在 StretchBlt 调用之前,根本没有任何函数涉及该图元文件的设备环境。所以让我们来看看 EMF4.EMF 是如何做到的,图 18-10 显示了该文件的部分内容。

图 18-10  EMF4 程序的部分十六进制转储

        此图元文件只有三条记录——一条头记录、一条 0x0E54 字节长的类型为 0x4D(对应 EMR_STRETCHBLT)的记录,以及一条文件结束记录。

        我也没法说请这条记录(译者注:这是指的是类型为 0x4D 的那条记录)里每一个字段的具体意思是什么。不过,我可以指出一个重要的关键点,它可以帮助理解 GDI 是如何把 EMF4.C 中的多个函数调用转换为只占用一条图元文件记录的。

        GDI 已经把原来与设备相关的位图转换为设备无关的位图(DIB)。这个记录存储了整个 DIB,所以记录才这么长。我会怀疑在显示该图元文件及位图的时候,GDI 实际上使用的是 StretchDIBits 函数而不是 StretchBlt 函数。或者,GDI 可能通过调用 CreateDIBitmap 函数把 DIB 转回设备相关位图,然后用内存设备环境和 StretchBlt 来显示。

        在图元文件中,EMR_STRETCHBLT 记录从偏移量 0x0088 开始。DIB 从图元文件中的偏移量 0x00F4 开始存储,一直到位于偏移量为 0x0EDC 的记录结尾处。DIB 以 40 字节的 BITMAPINFOHEADER 结构类型开头,紧接在其后的偏移量 0x011C 处的,是 22 行像素,每行有 40 个像素点。这是一个每像素 32 位的 DIB,所以每个像素需要占用 4 个字节。

18.2.5  枚举图元文件

        我们还可以通过枚举的方法来访问图元文件中的单个记录。图 18-11 所示的 EMF5 程序演示了这个过程。这个程序用一个图元文件来显示和 EMF3 一样的图像,但是使用的是枚举的方法。

/*------------------------------------------------
	EMF5.C -- Enhanced Metafile Demo #5
		  (c) Charles Petzold, 1998
------------------------------------------------*/

#include <Windows.h>

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
	PSTR szCmdLine, int iCmdShow)

	static TCHAR szAppName[] = TEXT("EMF5");
	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("Enhanced Metafile Demo #3"),
		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;


int CALLBACK EnhMetaFileProc(HDC hdc, HANDLETABLE * pHandleTable,
							CONST ENHMETARECORD * pEmfRecord,
							int iHandles, LPARAM pData)

	PlayEnhMetaFileRecord(hdc, pHandleTable, pEmfRecord, iHandles);

	return TRUE;


LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)

	HDC					hdc;
	HENHMETAFILE		hemf;
	PAINTSTRUCT			ps;
	RECT				rect;


	switch (message)
	
	case WM_PAINT:
		hdc = BeginPaint(hwnd, &ps);

		GetClientRect(hwnd, &rect);

		rect.left = rect.right / 4;
		rect.right = 3 * rect.right / 4;
		rect.top = rect.bottom / 4;
		rect.bottom = 3 * rect.bottom / 4;

		hemf = GetEnhMetaFile(TEXT("..\\\\emf3\\\\emf3.emf"));

		EnumEnhMetaFile(hdc, hemf, EnhMetaFileProc, NULL, &rect);
		DeleteEnhMetaFile(hemf);

		EndPaint(hwnd, &ps);
		return 0;

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	
	return DefWindowProc(hwnd, message, wParam, lParam);

        运行 EMF5 需要用到文件 EMF3.EMF,所以要先运行 EMF3 程序以创建这个文件。此外我们需要在 Visula C++ 的环境下运行这两个程序,以保证路径设置正确。这两个程序的主要不同在于:EMF3 调用 PlayEnhMetaFile 函数来完成 WM_PAINT 的消息处理,而 EMF5 调用的是 EnumEnhMetaFile 函数。你应该记得 PlayEnhMetaFile 的如下语法:

PlayEnhMetaFile(hdc, hemf, &rect);
第一个参数是要在其上显示图元文件的设备环境的句柄,第二个参数是增强型图元文件的句柄,第三个参数是 RECT 结构的指针,这个 RECT 结构定义了设备环境表面上的一个矩形。绘制出的图像会拉伸以填满这个矩形,但不会超出矩形的范围。

        EnumEnhMetaFile 函数有 5 个参数,其中的三个和 PlayEnhMetaFile 函数的一样(不过 RECT 结构的指针变成了最后一个参数)。

        EnumEnhMetaFile 的第三个参数是枚举函数的名字,我的程序里把它命名为 EnhMetaFileProc。第四个参数是一个指向任意数据类型的指针,它用来把数据传递到枚举函数中去。我这里简单地把它设置为 NULL。

        现在让我们来研究一下这里的枚举函数。在调用 EnumEnhMetaFile 函数时,GDI 会对图元文件中的每条记录调用一次 EnhMetaFileProc 函数,包括头记录和文件结束记录。通常枚举函数会返回 TRUE,如果返回 FALSE 的话整个枚举过程就会中止。

        这个枚举函数本身也有 5 个参数,我会一一简单介绍。在上面的程序中,我把前四个参数传递给 PalyEnhMetaFileRecord 函数,这个函数对一条图元记录执行相应的 GDI 操作,这和显示调用这些 GDI 的效果相同。

        EMF5 通过 EnumEnhMetaFile 和 PlayEnhMetaFileRecord 函数实现了与 EMF3 程序调用 PlayEnhMetaFile 函数一样的功能。不同的地方是,EMF5 在图元文件的显示进程中创建了一个钩子(hook),可以访问所有的图元文件记录。这个功能很有用。

        枚举函数的第一个参数是设备环境的句柄。GDI 简单地通过 EnumEnhMetaFile 的第一个参数来获取该句柄。我实现的枚举函数把该句柄传递给 PlayEnhMetaFileRecord 函数来标识用于显示图像的设备环境。

        让我跳过第二个参数,直接解释第三个参数。此参数是一个指向 ENHMETARECORD 类型的结构的指针,该结构前文已经解释过了。这个结构用来描述实际的图元记录,其内容就是图元文件中记录的实际编码。

        如果愿意,还可以写代码来检查这些记录。也许你选择不向 PlayEnhMetaFileRecord 函数传递有些记录。比如,在 EMF5.C 中,如果在 PlayEnhMetaFileRecord 语句前插入如下代码:

if (pEmfRecord->iType != EMR_LINETO)
重新编译并运行 EMF5 程序,可以看到只会显示一个矩形,以前存在的两条线则消失了。或者,如果在 PlayEnhMetaFileRecord 语句前插入如下代码,就会让程序使用系统默认的对象来显示图像,而不是用我们创建的画笔和画刷:

if (pEmfRecord->iType != EMR_SELECTOBJECT)
有一件事我们不应该做,那就是修改图元文件记录。但在你对此感到沮丧之前,让我们先来看看图 18-12 所示的程序 EMF6。

/*------------------------------------------------
	EMF6.C -- Enhanced Metafile Demo #6
				(c) Charles Petzold, 1998
------------------------------------------------*/

#include <Windows.h>

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
	PSTR szCmdLine, int iCmdShow)

	static TCHAR szAppName[] = TEXT("EMF6");
	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("Enhanced Metafile Demo #6"),
		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;


int CALLBACK EnhMetaFileProc(HDC hdc, HANDLETABLE * pHandleTable,
	CONST ENHMETARECORD * pEmfRecord,
	int iHandles, LPARAM pData)

	ENHMETARECORD * pEmfr;

	pEmfr = (ENHMETARECORD *)malloc(pEmfRecord->nSize);

	CopyMemory(pEmfr, pEmfRecord, pEmfRecord->nSize);

	if (pEmfr->iType == EMR_RECTANGLE)
		pEmfr->iType = EMR_ELLIPSE;

	PlayEnhMetaFileRecord(hdc, pHandleTable, pEmfr, iHandles);

	free(pEmfr);

	return TRUE;


LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)

	HDC					hdc;
	HENHMETAFILE		hemf;
	PAINTSTRUCT			ps;
	RECT				rect;


	switch (message)
	
	case WM_PAINT:
		hdc = BeginPaint(hwnd, &ps);

		GetClientRect(hwnd, &rect);

		rect.left = rect.right / 4;
		rect.right = 3 * rect.right / 4;
		rect.top = rect.bottom / 4;
		rect.bottom = 3 * rect.bottom / 4;

		hemf = GetEnhMetaFile(TEXT("..\\\\emf3\\\\emf3.emf"));

		EnumEnhMetaFile(hdc, hemf, EnhMetaFileProc, NULL, &rect);
		DeleteEnhMetaFile(hemf);

		EndPaint(hwnd, &ps);
		return 0;

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	
	return DefWindowProc(hwnd, message, wParam, lParam);

        和 EMF5 一样,EMF6 使用由 EMF3 创建的图元文件 EMF3.EMF,所以我们仍需运行 EMF3 来创建这个文件,还要记得得在 Visual C++ 环境里运行这两个程序。

        EMF6 程序演示了如何在显示图像之前更改图元记录,答案很简单:先做一份拷贝再对这份拷贝进行修改。如程序所示,枚举过程先用 malloc 分配一块大小和源图元文件记录一样的内存块,记录的大小可以从传给函数的 pEmfRecord 结构的 nSize 字段得到。指向该内存块的指针被存储在 pEmfr 变量里,pEmfr 变量是一个指向 ENHMETARECORD 结构的指针。

        使用函数 CopyMemory,程序把 pEmfRecord 指向的结构的内容拷贝到由 pEmfr 指向的结构中去。现在有东西可以让我们改动了。程序先检查记录的类型是否为 EMR_RECTANGLE,如果是的话,就把 iType 字段修改为 EMR_ELLIPSE。pEmfr 指针被传递给 PlayEnhMetaFileRecord 函数,然后被释放。其结果就是,该程序会绘制一个椭圆而非矩形。其余部分不变。

        当然,上面的改动相对简单,因为 Rectangle 函数和 Ellipse 函数所需要的参数都一样,而且它们都做同样的事情——为图像定义一个外框。如果要做更复杂的改动则需要更深入地了解不同图元文件记录的格式。

        我们还可以插入一个或两个额外的图元文件记录。例如,可以把 EMF6.C 中的 if 语句换成下面的代码:

if (pEmfr->iType == EMR_RECTANGLE)

        PlayEnhMetaFileRecord(hdc, pHandleTable, pEmfr, iHandles);
        pEmfr->iType = EMR_ELLIPSE;
这样,每次处理一个 Rectangle 记录,该程序就显示这个矩形,然后把该记录变为 Ellipse 再次显示它。这样程序就既画出了矩形也画出了椭圆。

        下面让我们研究一下在枚举图元文件时是如何处理 GDI 对象的。

        在图元文件头中,ENHMETAHEADER 结构中有一个 nHandles 字段,它用来记录这个文件中包含的非备用 GDI 对象的句柄数量。nHandles 的值比 GDI 对象的数量多 1,比如,EMF5 和 EMF6 用的图元文件中有一个画笔和一个画刷,nHandles 的值就是 3。而那个多出来的句柄我会在下文介绍。

        你可能会注意到,在 EMF5 和 EMF6 中,其枚举函数的倒数第二个参数的名字也叫 nHandles。它的值和上面提到的数一样,也是 3。

        枚举函数的第二个参数是指向一个名为 HANDLETABLE 结构的指针,该结构在 WINGDI.H 中的定义如下:

typedef struct tagHANDLETABLE

    HGDIOBJ objectHandle[1];

HANDLETABLE;

        其中的 HGDIOBJ 是一个指向 GDI 对象的 32 位通用指针,就和其他所有 GDI 对象一样。你还会注意到,这个结构中有一个只包含一个元素的数组字段,这意味着这个字段的长度其实是可变的。objectHandle 数组中元素的个数和 nHandles 相等,在本例中为 3。

        在枚举函数中,你可以用如下的表达式获取句柄:

pHandleTable->objectHandle[i]
这里,i 是 0、1 或 2,分别代表三个句柄。

        只要枚举函数被调用,此数组的第一个元素就将包含要被枚举的图元文件的句柄。这就是我上文提到的那个多出来的句柄。

        在枚举函数被第一次调用时,此数组中的第二和第三个元素会被设为 0,它们用于给画笔和画刷句柄预留位置。

        具体的工作过程如下:图元文件中的第一个对象创建函数有一个类型为 EMR_CREATEBRUSHINDIRECT 的记录,该记录指定了一个编号为 1 的记录。当这个记录被传递给 PlayEnhMetaFileRecord 函数后,GDI 就新建一个画刷,并得到一个指向它的句柄。这个句柄被存储在 objectHandle 数组中索引为 1 的位置(即第二个元素)。在第一条 EMR_SELECTOBJECT 记录被传给 PlayEnhMetaFileRecord 函数时,GDI 会看到句柄编号为 1,于是就从数组中得到其真实句柄值,并在 SelectObject 调用中使用它。当图元文件最终删除这个画刷时,GDI 会把 objectHandle 数组中索引为 1 的元素重设为 0。

        通过访问句柄数组 objectHandle,还可以用 GetObjectType 和 GetObject 这样的函数获取图元文件中所使用的对象的信息。

18.2.6  嵌入图像

        也许枚举图元文件最重要的应用就是将其他图像(或者甚至其他图元文件)嵌入现有的图元文件中。实际上,原有的图元文件不需被改变,我们只需创建一个新的图元文件并把源图元文件和要嵌入的图像合并起来。为此,基本的方法是把源图元文件的设备环境句柄当作第一个参数传递给函数 EnumEnhMetaFile,这样就可以在这个图元文件设备环境上同时显示图元文件记录和 GDI 函数调用了。

        最容易的嵌入方法是在图元文件命令序列的开始或结尾处嵌入新图像,就是说,在 EMR_HEADER 记录后或 EMF_EOF 记录前。当然,如果你很熟悉图元文件,也可以在任何位置插入新的绘图命令。程序 EMF7(图 18-13)演示了这个过程。

/*------------------------------------------------
	EMF7.C -- Enhanced Metafile Demo #7
		  (c) Charles Petzold, 1998
------------------------------------------------*/

#include <Windows.h>

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
	PSTR szCmdLine, int iCmdShow)

	static TCHAR szAppName[] = TEXT("EMF7");
	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("Enhanced Metafile Demo #7"),
		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;


int CALLBACK EnhMetaFileProc(HDC hdc, HANDLETABLE * pHandleTable,
	CONST ENHMETARECORD * pEmfRecord,
	int iHandles, LPARAM pData)

	HBRUSH		hBrush;
	HPEN		hPen;
	LOGBRUSH	lb;

	if (pEmfRecord->iType != EMR_HEADER && pEmfRecord->iType != EMR_EOF)
		PlayEnhMetaFileRecord(hdc, pHandleTable, pEmfRecord, iHandles);
	
	if (pEmfRecord->iType == EMR_RECTANGLE)
	
		hBrush = (HBRUSH)SelectObject(hdc, GetStockObject(NULL_BRUSH));
		
		lb.lbStyle = BS_SOLID;
		lb.lbColor = RGB(0, 255, 0);
		lb.lbHatch = 0;

		hPen = (HPEN)SelectObject(hdc,
			ExtCreatePen(PS_SOLID | PS_GEOMETRIC, 5, &lb, 0, NULL));

		Ellipse(hdc, 100, 100, 200, 200);

		DeleteObject(SelectObject(hdc, hPen));
		SelectObject(hdc, hBrush);
	
	return TRUE;


LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)

	ENHMETAHEADER		emh;
	HDC					hdc, hdcEMF;
	HENHMETAFILE		hemfOld, hemf;
	PAINTSTRUCT			ps;
	RECT				rect;


	switch (message)
	
	case WM_CREATE:
			
			// Retrieve existing metafile and header

		hemfOld = GetEnhMetaFile(TEXT("..\\\\emf3\\\\emf3.emf"));

		GetEnhMetaFileHeader(hemfOld, sizeof(ENHMETAHEADER), &emh);

			// Create a new metafile DC

		hdcEMF = CreateEnhMetaFile(NULL, TEXT("emf7.emf"), NULL,
									TEXT("EMF7\\0EMF Demo #7\\0"));

			// Enumerate the existing metafile

		EnumEnhMetaFile(hdcEMF, hemfOld, EnhMetaFileProc, NULL,
						(RECT *)& emh.rclBounds);

			// Clean up

		hemf = CloseEnhMetaFile(hdcEMF);

		DeleteEnhMetaFile(hemfOld);
		DeleteEnhMetaFile(hemf);
		return 0;

	case WM_PAINT:
		hdc = BeginPaint(hwnd, &ps);

		GetClientRect(hwnd, &rect);

		rect.left = rect.right / 4;
		rect.right = 3 * rect.right / 4;
		rect.top = rect.bottom / 4;
		rect.bottom = 3 * rect.bottom / 4;

		hemf = GetEnhMetaFile(TEXT("emf7.emf"));

		PlayEnhMetaFile(hdc, hemf, &rect);
		DeleteEnhMetaFile(hemf);

		EndPaint(hwnd, &ps);
		return 0;

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;
	
	return DefWindowProc(hwnd, message, wParam, lParam);

        EMF7 程序也使用了由 EMF3 程序创建的图元文件 EMF3.EMF,所以在运行 EMF7 之前仍然需要先运行 EMF3 来创建该图元文件。

        尽管 EMF7 中的 WM_PAINT 消息处理函数重新使用了 PlayEnhMetaFile 函数而非 EnumEnhMetaFile 函数,但 WM_CREATE 的消息处理却很不一样。

        首先,程序通过调用 GetEnhMetaFile 函数获取图元文件(EMF3.EMF)的句柄,并调用 GetEnhMetaFileHeader 获取增强型图元文件的头。获取图元文件头的唯一目的是因为在后续的 EnumEnhMetaFile 调用中需要使用 rclBounds 字段的信息。

        其次,程序创建了一个新的基于磁盘的图元文件 EMF7.EMF。函数 CreateEnhMetaFile 会返回该图元文件的设备环境句柄,然后这个句柄和 EMF3.EMF 的图元文件句柄将被用于 EnumEnhMetaFile 枚举函数。

        现在让我们看一下 EnhMetaFileProc 函数。如果被枚举的记录不是头记录或文件结束记录,函数就调用 PlayEnhMetaFileRecord 把这个记录转换到新的图元文件的设备环境上。(虽然不一定要去除图元文件的表头记录,但是保留表头记录会让新的图元文件变大。)

        在这个程序中,如果被枚举的记录是一个矩形绘制函数,我们的枚举函数就创建一个画笔并使用它绘制一个边框为绿色,背景是透明色的椭圆。请注意代码是如何通过保存画笔和画刷控点来恢复设备环境状态的。在此期间,所有这些函数都被嵌入新的图元文件中(记住,我们还可以用 PlayEnhMetaFile 函数将整个图元文件插入现有的文件)。

        最后,在 WM_CREATE 消息处理中,程序调用 CloseEnhMetaFile 函数关闭新的图元文件,并删除两个图元文件(EMF3.EMF 和 EMF7.EMF)的句柄,硬盘上仍然存储着这两个图元文件。

        从程序的显示结果我们可以明显地看出,绘制椭圆的操作是在绘制矩形的操作之后,但在绘制两条交叉线的操作之前。

18.2.7  增强型图元文件的查看和打印程序

        使用剪贴板传递增强型图元文件很方便。相应的剪贴板类型是 CF_ENHMETAFILE。GetClipboardData 函数返回一个增强型图元文件的句柄,SetClipboardData 函数可用于把一个图元文件的句柄放到剪贴板中。需要图元文件的副本?调用 CopyEnhMetaFile 函数即可。此外,如果把一个增强型的图元文件放在剪贴板中,Windows 会为需要它的程序使其应用于老格式的图元文件;同理,如果剪贴板中放的是老格式的图元文件,Windows 会使其成为增强型格式,Windows 也可以提供旧格式的图元文件。

        图 18-14 所示的 EMFVIEW 程序显示了如何把图元文件传入和传出剪贴板。该代码还可以加载、存储和打印这些图元文件。

/*------------------------------------------------
	EMFVIEW.C -- View Enhanced Metafiles
			(c) Charles Petzold, 1998
------------------------------------------------*/

#include <Windows.h>
#include <commdlg.h>
#include "resource.h"

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

TCHAR szAppName[] = TEXT("EmfView");

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;

以上是关于18.2 增强型图元文件的主要内容,如果未能解决你的问题,请参考以下文章

Excel VBA - 复制图表并粘贴为增强的图元文件

jlsb5是啥尺寸

如何把Excel中的图转成Visio格式

Win32 API之绘图函数

富文本框显示文字长度 超出变色

P1638 逛画展(直尺法)