摘录于《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 = 1DWORD nSize;           // 结构的大小RECTL rclBounds;       // 以像素为单位的矩形边框RECTL rclFrame;        // 以 0.01mm 为单位的图像尺寸DWORD dSignature;      // ENHMETA_SIGNATURE = " EMF"DWORD nVersion;        // 0x00010000DWORD 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 98Rectangle(hdcEMF, 100, 100, 201, 201);elseRectangle(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 headerhemfOld = GetEnhMetaFile(TEXT("..\\emf3\\emf3.emf"));GetEnhMetaFileHeader(hemfOld, sizeof(ENHMETAHEADER), &emh);// Create a new metafile DChdcEMF = CreateEnhMetaFile(NULL, TEXT("emf7.emf"), NULL,TEXT("EMF7\0EMF Demo #7\0"));// Enumerate the existing metafileEnumEnhMetaFile(hdcEMF, hemfOld, EnhMetaFileProc, NULL,(RECT *)& emh.rclBounds);// Clean uphemf = 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;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("Enhanced Metafile Viewer"),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;
}HPALETTE CreatePaletteFromMetaFile(HENHMETAFILE hemf)
{HPALETTE    hPalette;int            iNum;LOGPALETTE * plp;if (!hemf)return NULL;if (0 == (iNum = GetEnhMetaFilePaletteEntries(hemf, 0, NULL)))return NULL;plp = (LOGPALETTE*)malloc(sizeof(LOGPALETTE) + (iNum - 1) * sizeof(PALETTEENTRY));plp->palVersion = 0x0300;plp->palNumEntries = iNum;GetEnhMetaFilePaletteEntries(hemf, iNum, plp->palPalEntry);hPalette = CreatePalette(plp);free(plp);return hPalette;
}LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{static DOCINFO     di = { sizeof(DOCINFO), TEXT("EmfView: Printing") };static HENHMETAFILE  hemf;static OPENFILENAME    ofn;static PRINTDLG     printdlg = { sizeof(PRINTDLG) };static TCHAR       szFileName[MAX_PATH], szTitleName[MAX_PATH];static TCHAR        szFilter[] =TEXT("Enhanced Metafiles (*.EMF)\0*.emf\0")TEXT("All Files (*.*)\0*.*\0\0");BOOL               bSuccess;ENHMETAHEADER      header;HDC                  hdc, hdcPrn;HENHMETAFILE        hemfCopy;HMENU              hMenu;HPALETTE          hPalette;int                    i, iLength, iEnable;PAINTSTRUCT         ps;RECT             rect;PTSTR              pBuffer;switch (message){case WM_CREATE:// Initialize OPENFILENAME structureofn.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("emf");ofn.lCustData = 0;ofn.lpfnHook = NULL;ofn.lpTemplateName = NULL;return 0;case WM_INITMENUPOPUP:hMenu = GetMenu(hwnd);iEnable = hemf ? MF_ENABLED : MF_GRAYED;EnableMenuItem(hMenu, IDM_FILE_SAVE_AS, iEnable);EnableMenuItem(hMenu, IDM_FILE_PRINT, iEnable);EnableMenuItem(hMenu, IDM_FILE_PROPERTIES, iEnable);EnableMenuItem(hMenu, IDM_EDIT_CUT, iEnable);EnableMenuItem(hMenu, IDM_EDIT_COPY, iEnable);EnableMenuItem(hMenu, IDM_EDIT_DELETE, iEnable);EnableMenuItem(hMenu, IDM_EDIT_PASTE,IsClipboardFormatAvailable(CF_ENHMETAFILE) ?MF_ENABLED : MF_GRAYED);return 0;case WM_COMMAND:switch (LOWORD(wParam)){case IDM_FILE_OPEN:// Show the File Open dialog boxofn.Flags = 0;if (!GetOpenFileName(&ofn))return 0;// If there's an existing EMF, get rid of it.if (hemf){DeleteEnhMetaFile(hemf);hemf = NULL;}// Load the EMF into memroySetCursor(LoadCursor(NULL, IDC_WAIT));ShowCursor(TRUE);hemf = GetEnhMetaFile(szFileName);ShowCursor(FALSE);SetCursor(LoadCursor(NULL, IDC_ARROW));// Invalidate the client area for later updateInvalidateRect(hwnd, NULL, TRUE);if (hemf == NULL){MessageBox(hwnd, TEXT("Cannot load metafile"),szAppName, MB_ICONEXCLAMATION | MB_OK);}return 0;case IDM_FILE_SAVE_AS:if (!hemf)return 0;// Show the File Save dialog boxofn.Flags = OFN_OVERWRITEPROMPT;if (!GetSaveFileName(&ofn))return 0;// Sace the EMF to disk fileSetCursor(LoadCursor(NULL, IDC_WAIT));ShowCursor(TRUE);hemfCopy = CopyEnhMetaFile(hemf, szFileName);ShowCursor(FALSE);SetCursor(LoadCursor(NULL, IDC_ARROW));if (hemfCopy){DeleteEnhMetaFile(hemf);hemf = hemfCopy;}elseMessageBox(hwnd, TEXT("Cannot save metafile"),szAppName, MB_ICONEXCLAMATION | MB_OK);return 0;case IDM_FILE_PRINT:// Show the Print dialog box and get printer DCprintdlg.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;}// Get size of printable area of pagerect.left = 0;rect.right = GetDeviceCaps(hdcPrn, HORZRES);rect.top = 0;rect.bottom = GetDeviceCaps(hdcPrn, VERTRES);bSuccess = FALSE;// Play the EMF to the printerSetCursor(LoadCursor(NULL, IDC_WAIT));ShowCursor(TRUE);if ((StartDoc(hdcPrn, &di) > 0) && (StartPage(hdcPrn) > 0)){PlayEnhMetaFile(hdcPrn, hemf, &rect);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 metafile"),szAppName, MB_ICONEXCLAMATION | MB_OK);return 0;case IDM_FILE_PROPERTIES:if (!hemf)return 0;iLength = GetEnhMetaFileDescription(hemf, 0, NULL);pBuffer = (PSTR)malloc((iLength + 256) * sizeof(TCHAR));GetEnhMetaFileHeader(hemf, sizeof(ENHMETAHEADER), &header);// Format header file information i = wsprintf(pBuffer,TEXT("Bounds = (%i, %i) to (%i, %i) pixels\n"),header.rclBounds.left, header.rclBounds.top,header.rclBounds.right, header.rclBounds.bottom);i += wsprintf(pBuffer + i,TEXT("Frame = (%i, %i) to (%i, %i) mms\n"),header.rclFrame.left, header.rclFrame.top,header.rclFrame.right, header.rclFrame.bottom);i += wsprintf(pBuffer + i,TEXT("Resolution = (%i, %i) pixels")TEXT(" = (%i, %i) mms\n"),header.szlDevice.cx, header.szlDevice.cy,header.szlMillimeters.cx,header.szlMillimeters.cy);i += wsprintf(pBuffer + i,TEXT("Size = %i, Records = %i, ")TEXT("Handles = %i, Palette entries = %i\n"),header.nBytes, header.nRecords,header.nHandles, header.nPalEntries);// Include the metafile description, if presentif (iLength){i += wsprintf(pBuffer + i, TEXT("Description = "));GetEnhMetaFileDescription(hemf, iLength, pBuffer + i);pBuffer[lstrlen(pBuffer)] = '\t';}MessageBox(hwnd, pBuffer, TEXT("Metafile Properties"), MB_OK);free(pBuffer);return 0;case IDM_EDIT_COPY:case IDM_EDIT_CUT:if (!hemf)return 0;// Transfer metafile copy to the clipboardhemfCopy = CopyEnhMetaFile(hemf, NULL);OpenClipboard(hwnd);EmptyClipboard();SetClipboardData(CF_ENHMETAFILE, hemfCopy);CloseClipboard();if (LOWORD(wParam) == IDM_EDIT_COPY)return 0;// fall through if IDM_EDIT_CUTcase IDM_EDIT_DELETE:if (hemf){DeleteEnhMetaFile(hemf);hemf = NULL;InvalidateRect(hwnd, NULL, TRUE);}return 0;case IDM_EDIT_PASTE:OpenClipboard(hwnd);hemfCopy = (HENHMETAFILE)GetClipboardData(CF_ENHMETAFILE);CloseClipboard();if (hemfCopy && hemf){DeleteEnhMetaFile(hemf);hemf = NULL;}hemf = CopyEnhMetaFile(hemfCopy, NULL);InvalidateRect(hwnd, NULL, TRUE);return 0;case IDM_APP_ABOUT:MessageBox(hwnd, TEXT("Enhanced Metafile Viewer\n")TEXT("(c) Charles Petzold, 1998"),szAppName, MB_OK);return 0;case IDM_APP_EXIT:SendMessage(hwnd, WM_CLOSE, 0, 0L);return 0;}break;case WM_PAINT:hdc = BeginPaint(hwnd, &ps);if (hemf){if (hPalette = CreatePaletteFromMetaFile(hemf)){SelectPalette(hdc, hPalette, FALSE);RealizePalette(hdc);}GetClientRect(hwnd, &rect);PlayEnhMetaFile(hdc, hemf, &rect);if (hPalette)DeleteObject(hPalette);}EndPaint(hwnd, &ps);return 0;case WM_QUERYNEWPALETTE:if (!hemf || !(hPalette = CreatePaletteFromMetaFile(hemf)))return FALSE;hdc = GetDC(hwnd);SelectPalette(hdc, hPalette, FALSE);RealizePalette(hdc);InvalidateRect(hwnd, NULL, FALSE);DeleteObject(hPalette);ReleaseDC(hwnd, hdc);return TRUE;case WM_PALETTECHANGED:if ((HWND)wParam == hwnd)break;if (!hemf || !(hPalette = CreatePaletteFromMetaFile(hemf)))break;hdc = GetDC(hwnd);SelectPalette(hdc, hPalette, FALSE);RealizePalette(hdc);UpdateColors(hdc);DeleteObject(hPalette);ReleaseDC(hwnd, hdc);break;case WM_DESTROY:if (hemf)DeleteEnhMetaFile(hemf);PostQuitMessage(0);return 0;}return DefWindowProc(hwnd, message, wParam, lParam);
}
EMFVIEW.RC (excerpts)// Microsoft Visual C++ 生成的资源脚本。
//
#include "resource.h"/
//
// Menu
//EMFVIEW MENU DISCARDABLE
BEGINPOPUP "&File"BEGINMENUITEM "&Open\tCtrl+O", IDM_FILE_OPENMENUITEM "Save &As...", IDM_FILE_SAVE_ASMENUITEM SEPARATORMENUITEM "&Print...\tCtrl+P", IDM_FILE_PRINTMENUITEM SEPARATORMENUITEM "&Properties", IDM_FILE_PROPERTIESMENUITEM SEPARATORMENUITEM "E&xit", IDM_APP_EXITENDPOPUP "&Edit"BEGINMENUITEM "Cu&t\tCtrl+X", IDM_EDIT_CUTMENUITEM "&Copy\tCtrl+C", IDM_EDIT_COPYMENUITEM "&Paste\tCtrl+V", IDM_EDIT_PASTEMENUITEM "&Delete\tDel", IDM_EDIT_DELETEENDPOPUP "Help"BEGINMENUITEM "&About EmfView...", IDM_APP_ABOUTEND
END/
//
// Accelerator
//EMFVIEW ACCELERATORS DISCARDABLE
BEGIN"C",            IDM_EDIT_COPY,        VIRTKEY, CONTROL, NOINVERT"O",            IDM_FILE_OPEN,        VIRTKEY, CONTROL, NOINVERT"P",            IDM_FILE_PRINT,        VIRTKEY, CONTROL, NOINVERT"V",            IDM_EDIT_PASTE,        VIRTKEY, CONTROL, NOINVERTVK_DELETE,        IDM_EDIT_DELETE,    VIRTKEY, NOINVERT"X",            IDM_EDIT_CUT,        VIRTKEY, CONTROL, NOINVERT
END
RESOURCE.H (excerpts)// Microsoft Visual C++ generated include file.
// Used by EmfView.rc#define IDM_FILE_OPEN                   40001
#define IDM_FILE_SAVE_AS                40002
#define IDM_FILE_PRINT                  40003
#define IDM_FILE_PROPERTIES             40004
#define IDM_APP_EXIT                    40005
#define IDM_EDIT_CUT                    40006
#define IDM_EDIT_COPY                   40007
#define IDM_EDIT_PASTE                  40008
#define IDM_EDIT_DELETE                 40009
#define IDM_APP_ABOUT                   40010

EMFVIEW 实现了完整的调色板处理逻辑,这样做是因为有的图元文件可能包含一个调色板(通过调用 SelectPalette 函数)。在 WM_PAINT 中显示图元文件时,以及在 WM_QUERYNEWPALETTE 和 WM_PALETTECHANGED 的消息处理中,程序调用 CreatePaletteFromMetaFile 函数来得到图元文件中的调色板。

在处理菜单中的 Print 命令时,EMFVIEW 程序显示了一个标准的打印对话框,然后获取页面可打印区域的大小。图元文件会被放大或缩小以填满整个可打印区域。EMFVIEW 在它的窗口里显示图元文件时也是用的这种方式。

File 菜单中的 Properties 命令让 EMFVIEW 程序弹出一个消息框,其中包含了图元文件头的信息。

如果打印本章先前创建的 EMF2.EMF 图元文件,你会发现在高分辨率的打印机上线条的宽度非常细,甚至肉眼难以辨别。所以打印矢量图形,应该使用比较宽的画笔(比如宽度为 1磅)。本章后面的直尺的图像就使用了较宽的画笔。

18.2.8  显示精确的图元文件图像

使用图元文件的好处就是可以进行任意比例的缩放而不失真,这是因为图元文件通常包含的是一系列矢量图元(primitive),比如线条、填充区域和轮廓字体等,放大或缩小一个由图元文件定义的图像只需要按比例缩放这些矢量图元的坐标即可。而对于位图格式来说,在压缩时,必须要去掉整行或整列的像素,这会导致丢失一些重要信息。

当然,现实生活中的图元文件压缩也并非完美无缺。图像输出设备通常只有有限的像素尺寸,一个包含大量线条的图元文件图像在压缩过度的情况下看着就像是一个无法辨认的色块。还有,区域填充图案和色彩抖动(dither)效果也会在高度压缩下看起来很奇怪。此外,在图元文件包含嵌入的位图或传统的位图字体时,通常也会出现类似问题。

不过在大部分情况下,基于图元文件的图像可以自由缩放而不失真。在字处理软件和桌面排版文档中,需要插入图元文件时,这是个很有用的特性。通常,在这种软件中选择一个图元文件图像时,图像会被放置在一个方框内,可以使用鼠标拖动方框任意改变图像大小。在输出到打印机时,图像的尺寸也会相应地改变。

有时,允许任意缩放图元图像并不是一个好主意。比如在银行系统中,客户的签名图像被存储为一系列的多边形图元。只把图元文件变宽或只把它变长都会让签名看起来不一样。所以,在这种情况下,我们至少应该保证图像的纵横比保持不变。

在前面的范例程序中,图元图像的显示被限定于程序窗口的客户区(一个矩形区域)内。PlayEnhMetaFile 函数根据这个区域的大小对图像进行缩放。当改变窗口的大小时,图元文件的图像也会随之改变。这和我们在字处理程序中对图元文件图像的缩放操作是一样的。

要精确地显示图元文件的图像——无论是按特定的尺寸还是按恰当的纵横比——需要使用图元文件头里的尺寸信息并设置相应的矩形结构。

本章剩余部分的范例程序将用到一个叫 EMF.C 的外壳程序,它包含打印逻辑、一个资源描述文件 EMF.RC 和一个头文件 RESOURCE.H。图 18-15 显示了构成这个程序的文件。程序 EMF8.C 将利用这些代码显示一把 6 英寸长的直尺。

/*------------------------------------------------EMF8.C -- Enhanced Metafile Demo #8(c) Charles Petzold, 1998
------------------------------------------------*/#include <Windows.h>TCHAR szClass[] = TEXT("EMF8");
TCHAR szTitle[] = TEXT("EMF8: Enhanced Metafile Demo #8");void DrawRuler(HDC hdc, int cx, int cy)
{int        iAdj, i, iHeight;LOGFONT    lf;TCHAR    ch;iAdj = GetVersion() & 0x80000000 ? 0 : 1;// Black pen with 1-point widthSelectObject(hdc, CreatePen(PS_SOLID, cx / 72 / 6, 0));// Rectangle surrounding entire pen (with adjustment)Rectangle(hdc, iAdj, iAdj, cx + iAdj + 1, cy + iAdj + 1);// Tick marksfor (i = 1; i < 96; i++){if (i % 16 == 0) iHeight = cy / 2;  // incheselse if (i % 8 == 0) iHeight = cy / 3;  // half inches;else if (i % 4 == 0) iHeight = cy / 5;    // quarter incheselse if (i % 2 == 0) iHeight = cy / 8;  // eighthselse iHeight = cy / 12;  // sixteenthsMoveToEx(hdc, i * cx / 96, cy, NULL);LineTo(hdc, i * cx / 96, cy - iHeight);}// Create logical fontFillMemory(&lf, sizeof(lf), 0);lf.lfHeight = cy / 2;lstrcpy(lf.lfFaceName, TEXT("Times New Roman"));SelectObject(hdc, CreateFontIndirect(&lf));SetTextAlign(hdc, TA_BOTTOM | TA_CENTER);SetBkMode(hdc, TRANSPARENT);// Display numbersfor (i = 1; i <= 5; i++){ch = (TCHAR)(i + '0');TextOut(hdc, i * cx / 6, cy / 2, &ch, 1);}// Clean upDeleteObject(SelectObject(hdc, GetStockObject(SYSTEM_FONT)));DeleteObject(SelectObject(hdc, GetStockObject(BLACK_PEN)));
}void CreateRoutine(HWND hwnd)
{HDC                hdcEMF;HENHMETAFILE hemf;int                cxMms, cyMms, cxPix, cyPix, xDpi, yDpi;hdcEMF = CreateEnhMetaFile(NULL, TEXT("emf8.emf"), NULL,TEXT("EMF\0EMF Demo #8\0"));if (hdcEMF == NULL)return;cxMms = GetDeviceCaps(hdcEMF, HORZSIZE);cyMms = GetDeviceCaps(hdcEMF, VERTSIZE);cxPix = GetDeviceCaps(hdcEMF, HORZRES);cyPix = GetDeviceCaps(hdcEMF, VERTRES);xDpi = cxPix * 254 / cxMms / 10;yDpi = cyPix * 254 / cyMms / 10;DrawRuler(hdcEMF, 6 * xDpi, yDpi);hemf = CloseEnhMetaFile(hdcEMF);DeleteEnhMetaFile(hemf);
}void PaintRoutine(HWND hwnd, HDC hdc, int cxArea, int cyArea)
{ENHMETAHEADER  emh;HENHMETAFILE    hemf;int                cxImage, cyImage;RECT           rect;hemf = GetEnhMetaFile(TEXT("emf8.emf"));GetEnhMetaFileHeader(hemf, sizeof(emh), &emh);cxImage = emh.rclBounds.right - emh.rclBounds.left;cyImage = emh.rclBounds.bottom - emh.rclBounds.top;rect.left = (cxArea - cxImage) / 2;rect.right = (cxArea + cxImage) / 2;rect.top = (cyArea - cyImage) / 2;rect.bottom = (cyArea + cyImage) / 2;PlayEnhMetaFile(hdc, hemf, &rect);DeleteEnhMetaFile(hemf);
}
/*----------------------------------------------------------EMF.C -- Enhanced Metafile Demonstration Shell Program(c) Charles Petzold, 1998
----------------------------------------------------------*/#include <Windows.h>
#include <commdlg.h>
#include "resource.h"extern void CreateRoutine(HWND);
extern void PaintRoutine(HWND, HDC, int, int);LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);HANDLE    hInst;extern TCHAR szClass[];
extern TCHAR szTitle[];int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,PSTR szCmdLine, int iCmdShow)
{static TCHAR szResource[] = TEXT("EMF");HWND         hwnd;MSG             msg;WNDCLASS     wndclass;hInst = hInstance;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 = szResource;wndclass.lpszClassName = szClass;if (!RegisterClass(&wndclass)){MessageBox(NULL, TEXT("This program requires Windows NT!"),szClass, MB_ICONERROR);return 0;}hwnd = CreateWindow(szClass, szTitle,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;
}BOOL PrintRoutine(HWND hwnd)
{static DOCINFO    di;static PRINTDLG    printdlg = { sizeof(PRINTDLG) };static TCHAR    szMessage[32];BOOL            bSuccess = FALSE;HDC                hdcPrn;int                cxPage, cyPage;printdlg.Flags = PD_RETURNDC | PD_NOPAGENUMS | PD_NOSELECTION;if (!PrintDlg(&printdlg))return TRUE;if (NULL == (hdcPrn = printdlg.hDC))return FALSE;cxPage = GetDeviceCaps(hdcPrn, HORZRES);cyPage = GetDeviceCaps(hdcPrn, VERTRES);lstrcpy(szMessage, szClass);lstrcat(szMessage, TEXT(": Printing"));di.cbSize = sizeof(DOCINFO);di.lpszDocName = szMessage;if (StartDoc(hdcPrn, &di) > 0){if (StartPage(hdcPrn) > 0){PaintRoutine(hwnd, hdcPrn, cxPage, cyPage);if (EndPage(hdcPrn) > 0){EndDoc(hdcPrn);bSuccess = TRUE;}}}DeleteDC(hdcPrn);return bSuccess;
}LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{BOOL        bSuccess;static int    cxClient, cyClient;HDC            hdc;PAINTSTRUCT    ps;switch (message){case WM_CREATE:CreateRoutine(hwnd);return 0;case WM_COMMAND:switch (wParam){case IDM_PRINT:SetCursor(LoadCursor(NULL, IDC_WAIT));ShowCursor(TRUE);bSuccess = PrintRoutine(hwnd);ShowCursor(FALSE);SetCursor(LoadCursor(NULL, IDC_ARROW));if (!bSuccess)MessageBox(hwnd,TEXT("Error encountered during printing"),szClass, MB_ICONASTERISK | MB_OK);return 0;case IDM_EXIT:SendMessage(hwnd, WM_CLOSE, 0, 0);return 0;case IDM_ABOUT:MessageBox(hwnd, TEXT("Enhanced Metafile Demo Programe\n")TEXT("Copyright (c) Charles Petzold, 1998"),szClass, MB_ICONINFORMATION | MB_OK);return 0;}break;case WM_SIZE:cxClient = LOWORD(lParam);cyClient = HIWORD(lParam);return 0;case WM_PAINT:hdc = BeginPaint(hwnd, &ps);PaintRoutine(hwnd, hdc, cxClient, cyClient);EndPaint(hwnd, &ps);return 0;case WM_DESTROY:PostQuitMessage(0);return 0;}return DefWindowProc(hwnd, message, wParam, lParam);
}
EMF.RC (excerpts)// Microsoft Visual C++ 生成的资源脚本。
//
#include "resource.h"/
//
// Menu
//EMF MENU DISCARDABLE
BEGINPOPUP "&File"BEGINMENUITEM "&Print...", IDM_PRINTMENUITEM SEPARATORMENUITEM "E&xit", IDM_EXITENDPOPUP "&Help"BEGINMENUITEM "&About...", IDM_ABOUTEND
END
RESOURCE.H (excerpts)// Microsoft Visual C++ generated include file.
// Used by Emf.rc#define IDM_PRINT                       40001
#define IDM_EXIT                        40002
#define IDM_ABOUT                       40003

在 WM_CREATE 的消息处理中,EMF.C 调用了一个外部函数 CreateRoutine。该函数创建一个图元文件。EMF.C 还在两个地方调用了一个名为 PaintRoutine 的函数:分别是在 WM_PAINT 的消息处理和 PrintRoutine 函数中。PrintRoutine 函数用于响应菜单中的打印图像命令。

现代的打印机通常比视频显示设备有高得多的分辨率,所以我们可以利用打印效果来衡量按照特定尺寸绘制图元文件图像的能力。程序 EMF8 创建了一个需要按照特定尺寸显示的图元图像:一把 6 英寸长 1 英寸高的直尺。在这把直尺中,每 1/16 英寸有一个刻度,并使用 TrueType 字体来显示整数英寸的刻度值(1~5)。

为了画一把 6 英寸长的直尺,我们首先需要知道设备的分辨率。EMF8.C 中的 CreateRoutine 函数首先创建了一个图元文件,然后使用由 CreateEnhMetaFile 返回的设备环境句柄调用了 4 此 GetDeviceCaps 函数。这些调用获取显示表面的宽度和高度,分别使用 mm 和像素为单位。

这听起来很奇怪。图元文件的设备环境通常被看做一个存储 GDI 命令的中介。它并不是一个像视频显示或打印机那样的真正的显示设备,所以它怎么会有宽度和高度呢?

我们回忆一下 CreateEnhMetaFile 函数,它的第一个参数被认为是“参考设备环境”,GDI 利用它来建立图元文件的设备属性。如果这个参数被设为 NULL(如程序 EMF8),GDI 将使用视频显示设备作为这个参考设备环境。因此,当 EMF8 用该图元文件设备环境调用 GetDeviceCaps 函数时,它实际得到的是视频显示设备环境的信息。

程序 EMF8.C 使用以下方式计算每英寸含有的像素点数(分辨率):把以像素为单位计量的显示设备宽度除以以 mm 为单位计量的宽度,再乘以 25.4(一英寸包含的 mm 数)。

尽管我们已经为按正确的尺寸显示这把图元文件的支持做了很多事情,但我们的工作还没有完成。在显示图像的时候,PlayEnhMetaFile 函数会以它最后那个参数所定义的矩形来缩放图像。因此这个矩形必须被设定为支持的实际大小。

因此,EMF8 中的 PaintRoutine 函数调用了 GetEnhMetaFileHeader 函数来获取图元文件中的头信息。ENHMETATEHEADER 结构中的 rclBounds 字段指定了以像素为单位的图元文件图像的矩形边框。EMF8 程序利用这些信息将图元图像设定在客户区的中心,如图 18-16 所示。

图 18-16  程序 EMF8 的显示

请记住,显示在屏幕上的支持不可能绝对精确。正如我们在第 5 章所说的,视频显示只是近似于实际的尺寸。

上面这种技术看起来效果不错,但现在让我们试试打印这个图像。假设你有一台 300 dpi 的激光打印机,打出来的支持大概有 4/3 英寸宽。这是因为我们用了基于视频显示设备的像素尺寸。虽然这把“微型”的支持看起来不错,但它不是我们想要的。让我们再来试试。

ENHMETAHEADER 结构包含两个描述图像尺寸的矩形结构。EMF8 程序使用的是第一个,就是哪个 rclBounds 字段,它定义了图像的像素尺寸。第二个是 rclFrame 字段,它定义了图像的物理尺寸(以 0.01mm 为单位)。这两种尺寸之间的关系由参考设备环境决定,这个设备环境是我们在创建图元文件时指定的,在上面的例子中就是视频显示设备。(图元文件头还包括两个分别叫 szlDevice 和 szlMillimeters 的字段,它们都是 SIZEL 类型的结构,分别定义了参考设备的像素尺寸和物理尺寸,其值同样可以通过 GetDeviceCaps 函数获取。)

程序 EMF9 利用了图像的物理尺寸,具体可参见图 18-17 的代码

/*------------------------------------------------EMF9.C -- Enhanced Metafile Demo #9(c) Charles Petzold, 1998
------------------------------------------------*/#include <windows.h>
#include <string.h>TCHAR szClass[] = TEXT("EMF9");
TCHAR szTitle[] = TEXT("EMF9: Enhanced Metafile Demo #9");void CreateRoutine(HWND hwnd)
{
}void PaintRoutine(HWND hwnd, HDC hdc, int cxArea, int cyArea)
{ENHMETAHEADER  emh;HENHMETAFILE    hemf;int                cxMms, cyMms, cxPix, cyPix, cxImage, cyImage;RECT           rect;cxMms = GetDeviceCaps (hdc, HORZSIZE) ;cyMms = GetDeviceCaps (hdc, VERTSIZE) ;cxPix = GetDeviceCaps (hdc, HORZRES) ;cyPix = GetDeviceCaps (hdc, VERTRES) ;hemf = GetEnhMetaFile(TEXT("..\\emf8\\emf8.emf"));GetEnhMetaFileHeader(hemf, sizeof(emh), &emh);cxImage = emh.rclFrame.right - emh.rclFrame.left;cyImage = emh.rclFrame.bottom - emh.rclFrame.top;cxImage = cxImage * cxPix / cxMms / 100 ;cyImage = cyImage * cyPix / cyMms / 100 ;rect.left = (cxArea - cxImage) / 2;rect.right = (cxArea + cxImage) / 2;rect.top = (cyArea - cyImage) / 2;rect.bottom = (cyArea + cyImage) / 2;PlayEnhMetaFile(hdc, hemf, &rect);DeleteEnhMetaFile(hemf);
}

EMF9 使用由程序 EMF8 创建的图元文件,所以需要先运行 EMF8 来创建这个文件。

EMF9 中的 PaintRoutine 函数一开始用目标设备环境调用了 4 次 GetDeviceCaps 函数。和 EMF8 中的 CreateRoutine 函数一样,这些调用提供了设备的分辨率。在得到图元文件句柄后,它获取图元文件的头结构,并使用 rclFrame 字段计算出图元文件图像的物理尺寸(以 0.01mm 为单位)。这是第一步。

接下来,这个函数将此尺寸转换为像素:用输出设备的像素尺寸乘以前面得到的值,再除以物理尺寸,再除以 100,就可以得到用 0.01mm 为单位的像素的数量。现在 PaintRoutine 已经是以像素为单位的直尺长度,但该长度并不是特定于所使用的显示设备的,而是根据目标设备的尺寸计算出来的。利用这些信息,我们也可以很容易地把图像显示在目标设备的中间。

EMF9 在屏幕上的显示结果看起来和程序 EMF8 是一样的,但是如果你把它打印出来,那么你会看到,EMF9 的结果更接近一把正常的 6 英寸宽 1 英寸高的直尺。

18.2.9  缩放比例和纵横比

有时可能想利用 EMF8 程序产生的直尺图元文件,但并不需要显示一个 6 英寸长的图像。同时,如果仍然能保持图像具有 6 : 1 的正确纵横比那就最好不过了。正如前面讲过的那样,在字处理(或类似的)程序中,使用边框来改变图元文件的显示尺寸可能很方便,但有时会造成不希望的变形。在这些程序中,应该给用户提供一个选项,使得无论如何改变图像的尺寸,原有的图像纵横比都保持不变。也就是说,用户选择的边框并不直接用来定义传递给 PlayEnhMetaFile 函数的矩形结构。该函数使用的矩形结构可能仅仅使用了边框的部分信息。

程序 EMF10(图 18-18)给出了一个这样的例子。

/*------------------------------------------------EMF10.C -- Enhanced Metafile Demo #10(c) Charles Petzold, 1998
------------------------------------------------*/#include <windows.h>TCHAR szClass[] = TEXT("EMF10");
TCHAR szTitle[] = TEXT("EMF10: Enhanced Metafile Demo #10");void CreateRoutine(HWND hwnd)
{
}void PaintRoutine(HWND hwnd, HDC hdc, int cxArea, int cyArea)
{ENHMETAHEADER  emh;float           fScale;HENHMETAFILE hemf;int                cxMms, cyMms, cxPix, cyPix, cxImage, cyImage;RECT           rect;cxMms = GetDeviceCaps(hdc, HORZSIZE);cyMms = GetDeviceCaps(hdc, VERTSIZE);cxPix = GetDeviceCaps(hdc, HORZRES);cyPix = GetDeviceCaps(hdc, VERTRES);hemf = GetEnhMetaFile(TEXT("..\\emf8\\emf8.emf"));GetEnhMetaFileHeader(hemf, sizeof(emh), &emh);cxImage = emh.rclFrame.right - emh.rclFrame.left;cyImage = emh.rclFrame.bottom - emh.rclFrame.top;cxImage = cxImage * cxPix / cxMms / 100;cyImage = cyImage * cyPix / cyMms / 100;fScale = min((float)cxArea / cyImage, (float)cyArea / cyImage);cxImage = (int)(fScale * cxImage);cyImage = (int)(fScale * cyImage);rect.left = (cxArea - cxImage) / 2;rect.right = (cxArea + cxImage) / 2;rect.top = (cyArea - cyImage) / 2;rect.bottom = (cyArea + cyImage) / 2;PlayEnhMetaFile(hdc, hemf, &rect);DeleteEnhMetaFile(hemf);
}

程序 EMF10 将直尺缩放,让它的显示适应程序客户区(或者打印纸的可打印区域)的大小,同时保持原图像的纵横比。通常会看到这个直尺水平方向是撑满的,并在垂直方向上处于中间的位置。如果你把客户区设置得非常扁,直尺就会顶满客户区的上下边界,而在水平位置上则处于中间位置。

有很多方法可以用来计算正确的矩形边框,但我决定修改程序 EMF9 的代码来实现这一点。程序 EMF10.C 的 PaintRoutine 函数开始有点类似于 EMF9.C,它们都为目标设备环境计算出显示 6 英寸宽图像所需的像素数目。

然后在程序中,我们分别计算原图像和客户区的长度和宽度的比例,然后取其中较小的一个值作为最终的图像纵横比。这个值就是 fScale,它是一个浮点数。之后,在计算矩形边框之前,程序使用这个浮点数来扩大图像的像素尺寸。

18.2.10  图元文件中的映射模式

在前面的例子中,我们绘制了一直以英寸为单位的直尺,同时我们又用 mm 为单位来度量长度。这些工作似乎可以使用 GDI 提供的不同映射模式来完成。但我一直坚持使用像素来“手工地”做这些计算,为什么呢?

简单地说,这是因为在图元文件中使用映射模式很容易带来混乱。下面让我们尝试一下。

在使用图元文件的设备环境来调用 SetMapMode 函数时,该函数会和其他 GDI 函数一样被编码到相应的图元文件中。图 18-19 所示的程序 EMF11 展示了这一过程。

/*------------------------------------------------EMF11.C -- Enhanced Metafile Demo #11(c) Charles Petzold, 1998
------------------------------------------------*/#include <Windows.h>TCHAR szClass[] = TEXT("EMF11");
TCHAR szTitle[] = TEXT("EMF11: Enhanced Metafile Demo #11");void DrawRuler(HDC hdc, int cx, int cy)
{int        i, iHeight;LOGFONT  lf;TCHAR    ch;// Black pen with 1-point widthSelectObject(hdc, CreatePen(PS_SOLID, cx / 72 / 6, 0));// Rectangle surrounding entire pen (with adjustment)if (GetVersion() & 0x80000000)                // Windows 98Rectangle(hdc, 0, -2, cx + 2, cy);elseRectangle(hdc, 0, -1, cx + 1, cy);// Tick marksfor (i = 1; i < 96; i++){if (i % 16 == 0) iHeight = cy / 2;    // incheselse if (i % 8 == 0) iHeight = cy / 3;  // half inches;else if (i % 4 == 0) iHeight = cy / 5;    // quarter incheselse if (i % 2 == 0) iHeight = cy / 8;  // eighthselse iHeight = cy / 12;  // sixteenthsMoveToEx (hdc, i * cx / 96, 0, NULL) ;LineTo   (hdc, i * cx / 96, iHeight) ;}// Create logical font FillMemory(&lf, sizeof(lf), 0);lf.lfHeight = cy / 2;lstrcpy(lf.lfFaceName, TEXT("Times New Roman"));SelectObject(hdc, CreateFontIndirect(&lf));SetTextAlign(hdc, TA_BOTTOM | TA_CENTER);SetBkMode(hdc, TRANSPARENT);// Display numbersfor (i = 1; i <= 5; i++){ch = (TCHAR)(i + '0');TextOut(hdc, i * cx / 6, cy / 2, &ch, 1);}// Clean upDeleteObject(SelectObject(hdc, GetStockObject(SYSTEM_FONT)));DeleteObject(SelectObject(hdc, GetStockObject(BLACK_PEN)));
}void CreateRoutine(HWND hwnd)
{HDC                hdcEMF;HENHMETAFILE hemf;hdcEMF = CreateEnhMetaFile(NULL, TEXT("emf11.emf"), NULL,TEXT("EMF11\0EMF Demo #11\0"));SetMapMode(hdcEMF, MM_LOENGLISH);DrawRuler(hdcEMF, 600, 100);hemf = CloseEnhMetaFile(hdcEMF);DeleteEnhMetaFile(hemf);
}void PaintRoutine(HWND hwnd, HDC hdc, int cxArea, int cyArea)
{ENHMETAHEADER  emh;HENHMETAFILE    hemf;int                cxMms, cyMms, cxPix, cyPix, cxImage, cyImage;RECT           rect;cxMms = GetDeviceCaps(hdc, HORZSIZE);cyMms = GetDeviceCaps(hdc, VERTSIZE);cxPix = GetDeviceCaps(hdc, HORZRES);cyPix = GetDeviceCaps(hdc, VERTRES);hemf = GetEnhMetaFile(TEXT("emf11.emf"));GetEnhMetaFileHeader(hemf, sizeof(emh), &emh);cxImage = emh.rclFrame.right - emh.rclFrame.left;cyImage = emh.rclFrame.bottom - emh.rclFrame.top;cxImage = cxImage * cxPix / cxMms / 100;cyImage = cyImage * cyPix / cyMms / 100;rect.left = (cxArea - cxImage) / 2;rect.right = (cxArea + cxImage) / 2;rect.top = (cyArea - cyImage) / 2;rect.bottom = (cyArea + cyImage) / 2;PlayEnhMetaFile(hdc, hemf, &rect);DeleteEnhMetaFile(hemf);
}

EMF11 中的,CreateRoutine 函数比程序 EMF8 中的(最早的直尺图元文件程序)要简单,因为它不需要调用 GetDeviceCaps 函数刦获取视频设备每英寸的像素数。相反,EMF11 调用 SetMapMode 函数把映射模式设置为 MM_LOENGLISH。在这种模式下,每个逻辑单位被映射为 0.01 英寸。这样直尺的逻辑单位尺寸就是 600 * 100,这两个数值被传递到 DrawRuler 函数。

程序 EMF11 中的 DrawRuler 函数的实现和程序 EMF9 中的基本一样,只是 MoveToEx 和 LineTo 的参数有点不同。这两个函数被用来绘制刻度线,在以像素为单位进行绘制时(默认的 MM_TEXT 模式),纵坐标增加的方向是由上往下。而在 MM_LOENGLISH 方式(以及其他映射模式)中,纵坐标增强的方向是由下往上,所以需在参数上左略微的调整。同样还需要调整 Rectangle 函数所使用的参数。

程序 EMF11 中的 PaintRoutine 函数和程序 EMF9(该程序已经可以在显示器和打印机中使用正确的尺寸来显示直尺了)中的也基本一样。唯一的区别是程序 EMF11 用的是图元文件 EMF11.EMF,而 EMF9 用的是由 EMF8 创建的 EMF8.EMF 图元文件。

程序 EMF11 显示的图像和 EMF9 显示的基本一致。通过这个例子,我们可以学会如何使用 SetMapMode 函数去简化图元文件的创建,并同时确保不干扰用于接近正确尺寸显示图元文件的所有技巧。

18.2.11  使用映射模式显示图元文件

在程序 EMF11 中,计算目标矩形时需要调用多次 GetDeviceCaps 函数。我们的第二个目标是使用映射模式取代这些调用。GDI 把目标矩形的坐标看作是逻辑坐标,所以用 MM_HIMETRIC 模式来处理这些坐标看来是个好办法,因为这样会使每逻辑单位等于 0.01mm,而这正好就是增强型图元文件头中矩形边框所使用的单位。

图 18-20 所示的程序 EMF12 使用了源自 EMF8 程序的 DrawRuler 函数的算法,不同的是它使用 MM_HIMETRIC 映射模式来显示图元文件。

/*------------------------------------------------EMF12.C -- Enhanced Metafile Demo #12(c) Charles Petzold, 1998
------------------------------------------------*/#include <Windows.h>TCHAR szClass[] = TEXT("EMF12");
TCHAR szTitle[] = TEXT("EMF12: Enhanced Metafile Demo #12");void DrawRuler(HDC hdc, int cx, int cy)
{int        iAdj, i, iHeight;LOGFONT    lf;TCHAR    ch;iAdj = GetVersion() & 0x80000000 ? 0 : 1;// Black pen with 1-point widthSelectObject(hdc, CreatePen(PS_SOLID, cx / 72 / 6, 0));// Rectangle surrounding entire pen (with adjustment)Rectangle(hdc, iAdj, iAdj, cx + iAdj + 1, cy + iAdj + 1);// Tick marksfor (i = 1; i < 96; i++){if (i % 16 == 0) iHeight = cy / 2;  // incheselse if (i % 8 == 0) iHeight = cy / 3;  // half inches;else if (i % 4 == 0) iHeight = cy / 5;    // quarter incheselse if (i % 2 == 0) iHeight = cy / 8;  // eighthselse iHeight = cy / 12;  // sixteenthsMoveToEx(hdc, i * cx / 96, cy, NULL);LineTo(hdc, i * cx / 96, cy - iHeight);}// Create logical fontFillMemory(&lf, sizeof(lf), 0);lf.lfHeight = cy / 2;lstrcpy(lf.lfFaceName, TEXT("Times New Roman"));SelectObject(hdc, CreateFontIndirect(&lf));SetTextAlign(hdc, TA_BOTTOM | TA_CENTER);SetBkMode(hdc, TRANSPARENT);// Display numbersfor (i = 1; i <= 5; i++){ch = (TCHAR)(i + '0');TextOut(hdc, i * cx / 6, cy / 2, &ch, 1);}// Clean upDeleteObject(SelectObject(hdc, GetStockObject(SYSTEM_FONT)));DeleteObject(SelectObject(hdc, GetStockObject(BLACK_PEN)));
}void CreateRoutine(HWND hwnd)
{HDC                hdcEMF;HENHMETAFILE hemf;int                cxMms, cyMms, cxPix, cyPix, xDpi, yDpi;hdcEMF = CreateEnhMetaFile(NULL, TEXT("emf12.emf"), NULL,TEXT("EMF12\0EMF Demo #12\0"));cxMms = GetDeviceCaps(hdcEMF, HORZSIZE);cyMms = GetDeviceCaps(hdcEMF, VERTSIZE);cxPix = GetDeviceCaps(hdcEMF, HORZRES);cyPix = GetDeviceCaps(hdcEMF, VERTRES);xDpi = cxPix * 254 / cxMms / 10;yDpi = cyPix * 254 / cyMms / 10;DrawRuler(hdcEMF, 6 * xDpi, yDpi);hemf = CloseEnhMetaFile(hdcEMF);DeleteEnhMetaFile(hemf);
}void PaintRoutine(HWND hwnd, HDC hdc, int cxArea, int cyArea)
{ENHMETAHEADER  emh;HENHMETAFILE    hemf;POINT          pt;int              cxImage, cyImage;RECT           rect;SetMapMode(hdc, MM_HIMETRIC);SetViewportOrgEx(hdc, 0, cyArea, NULL);pt.x = cxArea;pt.y = 0;DPtoLP(hdc, &pt, 1);hemf = GetEnhMetaFile(TEXT("emf12.emf"));GetEnhMetaFileHeader(hemf, sizeof(emh), &emh);cxImage = emh.rclFrame.right - emh.rclFrame.left;cyImage = emh.rclFrame.bottom - emh.rclFrame.top;rect.left = (pt.x - cxImage) / 2;rect.right = (pt.x + cxImage) / 2;rect.top = (pt.y + cyImage) / 2;rect.bottom = (pt.y - cyImage) / 2;PlayEnhMetaFile(hdc, hemf, &rect);DeleteEnhMetaFile(hemf);
}

EMF12 中的 PaintRoutine 函数先将映射模式设置为 MM_HIMETRIC。跟其他度量模式一样,此时 y 坐标的增长方向是从下往上。但是,原点仍然在左上角,这意味着客户区的 y 坐标都是负数。为了便于操作,我们使用 SetViewportOrgEx 函数把坐标原点做左上角设置到左下角。

设备坐标点(cxArea, 0)位于屏幕的右上角。我们把这个坐标传递给函数 DPtoLP(“device point to logical point”),便可以得到以 0.01mm 为单位的客户区尺寸。

接下来,程序加载图元文件,读取文件头并获取图像的尺寸(同样以 0.01mm 为单位)。之后,位于客户区中央的目标矩形就很容易计算出来了。

现在我们已经知道了如何使用映射模式来创建一个图元文件,也了解了如何使用映射模式显示图元文件。这两样可以同时进行吗?

正如图 18-21 中的 EMF13 程序所示,这是可行的。

/*------------------------------------------------EMF13.C -- Enhanced Metafile Demo #13(c) Charles Petzold, 1998
------------------------------------------------*/#include <Windows.h>TCHAR szClass[] = TEXT("EMF13");
TCHAR szTitle[] = TEXT("EMF13: Enhanced Metafile Demo #13");void CreateRoutine(HWND hwnd)
{
}void PaintRoutine(HWND hwnd, HDC hdc, int cxArea, int cyArea)
{ENHMETAHEADER  emh;HENHMETAFILE    hemf;POINT          pt;int              cxImage, cyImage;RECT           rect;SetMapMode(hdc, MM_HIMETRIC);SetViewportOrgEx(hdc, 0, cyArea, NULL);pt.x = cxArea;pt.y = 0;DPtoLP(hdc, &pt, 1);hemf = GetEnhMetaFile(TEXT("..\\emf11\\emf11.emf"));GetEnhMetaFileHeader(hemf, sizeof(emh), &emh);cxImage = emh.rclFrame.right - emh.rclFrame.left;cyImage = emh.rclFrame.bottom - emh.rclFrame.top;rect.left = (pt.x - cxImage) / 2;rect.right = (pt.x + cxImage) / 2;rect.top = (pt.y + cyImage) / 2;rect.bottom = (pt.y - cyImage) / 2;PlayEnhMetaFile(hdc, hemf, &rect);DeleteEnhMetaFile(hemf);
}

在程序 EMF13 里,不需要使用映射模式来创建直尺的图元文件,因为后者已经由程序 EMF11 创建了。EMF13 只是加载了该图元文件,并利用映射模式来计算目标矩形,就像 EMF12 做的那样。

现在我们可以总结出几条原则。在创建图元文件时,GDI 根据映射模式内嵌的变化来以像素和 mm 为单位计算图元文件图像的尺寸。图像的尺寸存储在图元文件的头结构中。当现实图像时,GDI 根据调用 PlayEnhMetaFile 函数时使用的映射模式来确定图像的目标矩形的实际物理位置。图元文件中的任何内容都不能改变这个位置。

18.2 增强型图元文件相关推荐

  1. iconfont 图标宽高出问题_一个技巧,100,000,000+PPT图标就可以任性使用!【黑科技第11期】...

    #Hello,我是安少# 视觉传达展示好,巧用图标不能少. 提及图标,大家应该都不陌生,生活中处处可见,机场路牌.道路指示.手机App等等,小图标,大作用,一个图标能快速简明扼要的传递信息. (交通指 ...

  2. 江苏成教计算机统考操作题多少分,江苏省成人计算机统考试题,操作题.doc

    江苏省成人计算机统考试题<操作题> 二.操作题(60分) 1.调入考生文件夹中的ED.RTF文件,参考样张(附后)按下列要求进行操作. (1)将页面设置为:16开纸,左.右页边距均为2厘米 ...

  3. visio2013复制到word有多余白边_学习工坊(一)|实用技巧之Word篇

    Word那些你不知道的实用小技巧 让你工作更快捷 大家无论学习还是工作 总离不开Word软件吧 今天推给大家几个实用的Word小技巧 从此快人一步 开启Word新世界 1.Word的分屏 在编辑Wor ...

  4. Win32 API 函数列表

    ID编号 函数名 函数说明 详细说明 Win16支持 Win9x支持 WinNT支持 1 AbortDoc 终止一项打印作业  Yes Yes Yes 2 AbortPath 终止或取消DC中的一切路 ...

  5. Win32 API 函数列表1(格式有点乱)

    西安邮电学院 徐兆元 ID编号 函数名 函数说明 详细说明 Win16支持 Win9x支持 WinNT支持 1 AbortDoc 终止一项打印作业  Yes Yes Yes 2 AbortPath 终 ...

  6. 数组和广义表 - [数据结构]

    2005-09-07 数组和广义表 - [数据结构] 第五章 数组和广义表 --非线性数据结构 5.1 数组的定义和运算 ☆二维数组的逻辑结构形式定义为: 2_Array=( D, R ) 其中 D= ...

  7. 用记事本怎么在html页面中加入个按钮,,页面提示5次你干啥点我,用word制作网页...

    1.即时取消 Word 的后台打印 当我们刚刚编辑完一篇文档按了打印命令后,后来又不想打印当前的文档了,那么怎样才能即时取消后台打印任务呢?一般来说,大家在发出打印任务后,程序会自动将打印任务设置为后 ...

  8. 最新Word及Excel操作技巧

    1.即时取消Word的后台打印:当我们刚刚编辑完一篇文档按了打印命令后,后来又不想打印当前的文档了,那么怎样才能即时取消后台打印任务呢?一般来说,大家在发出打印任务后,程序会自动将打印任务设置为后台打 ...

  9. DELPHI 打印预览功能

    在很多应用程序中,都需要程序具有打印预览功能,以避免用户由于选择不当出现打印错误. 预览实现方式为通过创建一个Tpanel的派生类并公开它的canvas属性比例尺或视区范围,使用较为不方便,笔者通过实 ...

最新文章

  1. android 常用的监听器,Android中的Keyboard监听事件
  2. Android 机型适配之gradient默认渐变方向
  3. SpringMvc+AngularJS通过CORS实现跨域方案
  4. Webview离线功能(优先cache缓存+cache缓存管理)
  5. 【HTML5】Canvas画布
  6. 操作系统中的一些基本概念
  7. [pl-slam] 几个重要的参数属性
  8. Cesium加载OSGB数据
  9. Matlab GUI的文件打开和保存uigetfile uigetdir
  10. 7月22日自助装机配置专家点评
  11. 谷歌浏览器设置启动页被hao123劫持_win10系统打开chrome会被hao123劫持怎么办?解决方案...
  12. 谈判如何在谈判中_工资谈判软件开发人员指南
  13. android 读取excel 文件
  14. pmw.php,加速调光频率 PWM实现精准LED调光
  15. RGB三原色的简单理解
  16. 2018 焦作 onsite E - Resistors in Parallel(数学或规律+大数)
  17. 快速傅里叶变换-快速傅里叶变换
  18. hdu 1983 Kaitou Kid - The Phantom Thief (2)【Bfs+暴力枚举】
  19. 关于中科院力学所怀柔试验基地被非法拆毁的严正声明 ZZ
  20. Bypass部分知识

热门文章

  1. MinGW32编译ffmpeg+libsrt
  2. 通过Safari浏览器获取iOS设备UDID(设备唯一标识符)
  3. Domoticz添加实时天气信息显示
  4. DOSBox使用总结——调整DOSBox窗口并自动挂载指定目录
  5. 电商平台商品订单拆分模式分析
  6. 如何查看Android API文档
  7. STM32CubeIDE
  8. python3计算常数e的代码
  9. 多源传感器融合时的时间对齐或者时间同步问题
  10. Contour Integral