摘录于《Windows程序(第5版,珍藏版).CHarles.Petzold 著》P845
图元文件可以被临时存储在内存中,也可以被存储为磁盘文件。对于应用程序来说,这两个过程很相似;因为 Windows 会处理所有从图元文件加载数据和把它保存到磁盘的文件输入/输出操作。
18.1.1 内存图元文件的简单用法
要想创建老式的图元文件格式,应首先调用 CreateMetaFile 函数来创建图元文件的设备环境。然后就可以使用大多数 GDI 绘图函数在这个图元文件的设备环境上绘图。不过,调用这些 GDI 并不是真的在此设备上绘图,绘图的各个操作其实被存储在该图元文件中。当关闭图元文件的设备环境后,会返回这个图元文件的句柄。如此一来,你就可以在实际的设备环境上显示该图元文件,这跟直接执行图元文件中的 GDI 函数完全一样。
CreateMetaFile 函数只有一个参数,它可以是 NULL 或是一个文件名。如果参数为 NULL,表示图元文件被存储在内存中,如果是一个扩展名为 .WMF(表示 Windows MetaFile)的文件名,则表示图元文件要被存储在磁盘文件中。
图 18-1 所示的 METAFILE 程序显示了如何在处理 WM_CREATE 消息时产生一个内存图元文件,并在处理 WM_PAINT 消息时显示图像 100 次。
/*------------------------------------------------
metafile.C -- Metafile Demostration Program
(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("Metafile");
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("Metafile Demonstration"),
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 HMETAFILE hmf;
static int cxClient, cyClient;
HBRUSH hBrush;
HDC hdc, hdcMeta;
int x, y;
PAINTSTRUCT ps;
switch (message)
{
case WM_CREATE:
hdcMeta = CreateMetaFile(NULL);
hBrush = CreateSolidBrush(RGB(0, 0, 255));
Rectangle(hdcMeta, 0, 0, 100, 100);
MoveToEx(hdcMeta, 0, 0, NULL);
LineTo(hdcMeta, 100, 100);
MoveToEx(hdcMeta, 0, 100, NULL);
LineTo(hdcMeta, 100, 0);
SelectObject(hdcMeta, hBrush);
Ellipse(hdcMeta, 20, 20, 80, 80);
hmf = CloseMetaFile(hdcMeta);
DeleteObject(hBrush);
return 0;
case WM_SIZE:
cxClient = LOWORD(lParam);
cyClient = HIWORD(lParam);
return 0;
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
SetMapMode(hdc, MM_ANISOTROPIC);
SetWindowExtEx(hdc, 1000, 1000, NULL);
SetViewportExtEx(hdc, cxClient, cyClient, NULL);
for (x = 0; x < 10; x++)
for (y = 0; y < 10; y++)
{
SetWindowOrgEx(hdc, -100 * x, -100 * y, NULL);
PlayMetaFile(hdc, hmf);
}
EndPaint(hwnd, &ps);
return 0;
case WM_DESTROY:
DeleteMetaFile(hmf);
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, message, wParam, lParam);
<pre name="code" class="cpp">}
这个程序演示了如何使用在处理内存图元文件时必须用到的四个图元文件函数。第一个是 CreateMetaFile 函数,程序在处理 WM_CREATE 消息时使用 NULL 参数来调用它。该函数返回一个图元文件设备环境的句柄。然后程序用此设备环境绘制两条线和一个蓝色椭圆。这些函数调用都以二进制形式存储在图元文件中。CloseMetaFile 函数返回该图元文件的句柄。请注意,因为此图元文件的句柄要在后面使用,所以要被存储在静态变量中。
该图元文件包含以二进制形式表示的一系列 GDI 函数调用,它们是:一个 Rectangle 调用、两个 MoveToEx 调用、两个 LineTo 调用、一个 SelectObject 调用(选用蓝色画刷)和一个 Ellipse 调用。坐标不表示任何映射模式或转换,它们只是简单地以数字形式存储在该图元文件中。
在处理 WM_PAINT 消息时,该程序设定一种映射模式并调用 PlayMetaFile 函数在窗口中绘制对象 100 次。图元文件中的函数的坐标,其具体含义将由目标设备环境的当前坐标转换设置来确定。调用 PlayMetaFile 的实际效果是重复最初在处理 WM_CREATE 消息并创建该图元文件时,从 CreateMetaFile 到 CloseMetaFile 之间的所有调用。
和其他 GDI 对象一样,图元文件对象在程序终止之前应该被删除。前例中,在处理 WM_DESTROY 消息时,通过调用 DeleteMetaFile 函数来完成这个操作。
图 18-2 显示了该程序的运行结果。
图 18-2 METAFILE 程序的运行结果
18.1.2 把图元文件存储到磁盘
在前例中,CreateMetaFile 函数中的 NULL 参数意味着要在内存里(临时地)创建图元文件。我们也可以创建以文件形式存储在磁盘上的图元文件,该方法对大图元文件比较合适,因为它占用较少的内存空间,缺点则是每次使用图元文件都需要访问一次磁盘。
要把图 18-1 的程序转换为使用基于磁盘的图元文件,需要把 CreateMetaFile 函数的 NULL 参数替换为一个文件名。在 WM_CREATE 处理结束时,可以把图元文件的句柄传递给 DeleteMetaFile 函数。这样就删除了句柄,但是磁盘文件仍然被保留。
在处理 WM_PAINT 消息的过程中,调用 GetMetaFile 函数可以获得该磁盘文件的图元文件句柄,如下所示:
hmf = GetMetaFile(szFileName);
现在就可以像之前那样使用该图元文件了。在处理完 WM_PAINT 消息后,可以使用如下语句删除图元文件的句柄:
DeleteMetaFile(hmf);
在处理 WM_DESTROY 消息时,不需要删除该图元文件,因此它已经在 WM_CREATE 消息和每个 WM_PAINT 消息结束时被删除了。不过,除非要保留它的磁盘文件,否则应该使用如下语句删除该磁盘文件:
DeleteFile(szFileName);
可以像第 10 章中介绍的那样把图元文件定义为程序的资源,然后可以简单地把它作为一个数据块加载。如果已经有图元文件内容的数据块,可以使用如下语句来创建图元文件:
hmf = SetMetaFileBitsEx(iSize, pData);
和 SetMetaFileBitsEx 函数同一系列的有一个名为
GetMetaFileBitsEx 的函数,它会将图元文件的内容复制到内存块。
18.1.3 老式的图元文件和剪贴板
老式的图元文件有一个严重的缺陷。如果有一个老式的图元文件的句柄,如何在显示图元文件的时候确定图像有多大呢?除非深入了解图元文件的内部信息,否则没有办法知道。
还有,当一个程序从剪贴板获取一个老式图元文件时,如果后者被定义为用 MM_ISOTROPIC 或 MM_ANISOTROPIC 映射模式来显示,那么该程序在使用它时将拥有高度的灵活性。该程序在接收到图元文件后,可以简单地通过在显示之前设置视口范围来缩放图像。但如果在图元文件内部设置了 MM_ISOTROPIC 或 MM_ANISOTROPIC 映射模式,那么就收该图元文件的程序就会犯愁了。程序只能在显示图元文件之前或者之后调用 GDI,但不可以在显示图元文件期间调用 GDI。
要解决这些问题,就不能把老式图元文件句柄直接放入剪贴板给其他程序使用,而应该把它作为“图元文件图片”的一部分。图元文件图片是一种 METAFILEPICT 类型的结构,该结构允许从剪贴板获取图元文件图片的程序在显示图元文件之前设置映射模式和视口范围。
METAFILEPICT 结构为 16 个字节长,其定义如下:
typedef struct tagMETAFILEPICT
{
LONG mm; // mapping mode
LONG xExt; // width of the metafile image
LONG yExt; // height of the metafile image
LONG hMF; // handle ot the metafile
}
METAFILEPICT;
除了 MM_ISOTROPIC 和 MM_ANISOTROPIC,在其他所有的映射模式中,xExt 和 yExt 值都为图像的大小,它们的单位由 mm 字段所给定的映射模式指定。有了这个信息,从剪贴板复制图元文件图片结构的程序就可以确定显示该图片需要多少空间。而创建该图元文件的程序可以把这些值设置为图元文件里 GDI 绘制函数中所使用的所有 x 坐标和 y 坐标的最大值。
在 MM_ISOTROPIC 和 MM_ANISOTROPIC 映射模式中,xExt 和 yExt 值的功能有所不同。在第 5 章介绍过,当一个程序要想在 GDI 函数中使用跟图像大小无关的任意逻辑单位时,就要使用 MM_ISOTROPIC 或者 MM_ANISOTROPIC 映射模式。如果要保持一个与显示平面大小无关的纵横比,就用 MM_ISOTROPIC 模式;如果不在乎纵横比,可以使用 MM_ANISOTROPIC 模式。同样在第 5 章中讲过,一个程序把映射模式设置为 MM_ISOTROPIC 或 MM_ANISOTROPIC 后,通常还要调用 SetWindowExtEx 和 SetViewportExtEx 函数。SetWindowExtEx 函数用逻辑单位来指定该程序绘图时使用的单位。SetViewportExtEx 函数使用基于显示平面大小(例如窗口的客户区大小)的设备单位。
如果应用程序为剪贴板创建一个 MM_ISOTROPIC 或 MM_ANISOTROPIC 模式的图元文件,则该文件本身不能包含 SetViewportExtEx 调用。这是因为该调用中的设备单位是基于创建该图元文件的程序的显示表面的,而不是基于从剪贴板读取该图元文件的程序的显示表面的。相反,xExt 和 yExt 参数值应协助那个从剪贴板获得图元文件的程序设置适当的视口范围,以便显示图像。不过,当映射模式为 MM_ISOTROPIC 或 MM_ANIOSTROPIC 时,图元文件本身包含一个设置窗口范围的函数调用。该图元文件内的 GDI 绘图函数的坐标是基于这些窗口范围的。
创建图元文件和图元文件图片的程序应遵循以下规则。
- 用 METAFILEPICT 结构的 mm 字段来指定映射模式。
- 对于除 MM_ISOTROPIC 和 MM_ANISOTROPIC 以外的映射模式,xExt 和 yExt 字段用来指定图像的宽度和高度,其单位对应于 mm 字段的设置。要在 MM_ISOTROPIC 或 MM_ANISOTROPIC 模式下显示图元文件会稍微复杂一些。在 MM_ANISOTROPIC 模式下,如果程序不指定图像的尺寸或不固定图像的纵横比,则 xExt 及 yExt 值应设为 0。在 MM_IOSTROPIC 或 MM_ANIOSTROPIC 模式下,正的 xExt 与 yExt 值表示图像指定的宽度和高度,其单位为 0.01mm(MM_HIMETRIC 单位)。在 MM_ISOTROPIC 模式下,负的 xExt 与 yExt 值表示一个图像的指定纵横比,而不是尺寸。
- 在 MM_ISOTROPIC 和 MM_ANISOTROPIC 映射模式中,图元文件本身包含对 SetWindowExtEx 和(可能的)SetWindowOrgEx 的调用。就是说,创建该图元文件的程序会在图元文件的设备环境中调用这些函数。通常,图元文件不包含对 SetMapMode、SetViewportExtEx 或 SetViewportOrgEx 的调用。
- 图元文件应该是基于内存的,而不是基于磁盘的。
以下是一些示例代码,该程序创建一个图元文件并将其复制到剪贴板上。如果该图元文件使用 MM_ISOTROPIC 或 MM_ANISOTROPIC 映射模式,那么图元文件中的第一个调用应该是设置窗口范围。(窗口范围在其他映射模式中是固定的。)不管是哪种映射模式,我们还可以同时设置窗口原点:
hdcMeta = CreateMetaFile (NULL);
SetWindowExtEx(hdcMeta, ...);
SetWindowOrgEx(hdcMeta, ...);
在图元文件中,绘图函数中的坐标基于这些窗口范围和窗口原点。当该程序使用 GDI 调用在图元文件的设备环境上完成绘图后,将关闭图元文件以获取它的句柄:
hmf = CloseMetaFile(hdcMeta);
该程序还需要定义一个指向 METAFILEPICT 结构的指针,并为此结构分配一段全局内存块:
GLOBALHANDLE hGlobal;
LPMETAFILEPICT pMFP;
[其他程序行]
hGlobal = GlobalAlloc(GHND | GMEM_SHARE, sizeof(METAFILEPICT));
pMFP = (LPMETAFILEPICT) GlobalLock(hGlobal);
下一步,程序设置该结构的四个字段:
pMFP->mm = MM_...;
pMFP->xExt = ...;
pMFP->yExt = ...;
pMFP->hMF = hmf;
然后,程序把含有图元文件图片结构的全局内存块传输到剪贴板上:
OpenClipboard (hwnd);
EmptyClipboard();
SetClipboardData(CF_METAFILEPICT, hGlobal);
CloseClipboard();
在这些调用之后,hGlobal 句柄(含有图元文件图片结构的内存块)和 hmf 句柄(图元文件本身)在创建它们的程序中就失效了。
现在到了比较难的部分。当程序重剪贴板获取并显示图元文件时,必须进行以下步骤。
- 程序使用图元文件图片结构的 mm 字段来设置映射模式。
- 对于 MM_ISOTROPIC 或 MM_ANISOTROPIC 以外的映射模式,程序使用 xExt 和 yExt 值来设置剪辑矩形或者简单地确定图像的大小。对 MM_ISOTROPIC 和 MM_ANISOTROPIC 映射模式,程序使用 xExt 和 yExt 来设置视口范围。
- 然后程序就可以显示该图元文件了。
以下是程序代码。首先打开剪贴板,获得图元文件图片结构的句柄,并锁定它:
OpenClipboard (hwnd);
hGlobal = GetClipboardData(CF_METAFILEPICT);
pMFP = (LPMETAFILEPICT) GlobalLock(hGlobal);
然后保存当前设备环境的属性,并用结构的 mm 字段的值设置映射模式:
SaveDC (hdc);
SetMappingMode (pMFP->mm);
如果映射模式不是 MM_ISOTROPIC 或者 MM_ANISOTROPIC,那么你可以将剪辑矩形设置为 xExt 和 yExt 的值。因为这些值使用的是逻辑单位,所以必须使用 LPtoDP 将剪切矩形的坐标转换为设备单位。或者,也可以简单地把这些值保存起来,以便了解图像的大小。
对于 MM_ISOTROPIC 或 MM_ANISOTROPIC 映射模式,应使用 xExt 和 yExt 来设置视口范围。下面有一个可以完成这项任务的函数。该函数假定如果 xExt 和 yExt 没有指定视口范围的尺寸的话,cxClient 和 cyClient 就代表了图元文件显示区域的像素高度和像素宽度。
void PrepareMetaFile(HDC hdc, LPMETAFILEPICT pmfp,
int cxClient, int cyClient)
{
int xScale, yScale, iScale;
SetMapMode(hdc, pmfp->mm);
if (pmfp->mm == MM_ISOTROPIC || pmfp->mm == MM_ANISOTROPIC)
{
if (pmfp->xExt == 0)
SetViewportExtEx(hdc, cxClient, cyClient, NULL);
else if (pmfp->xExt > 0)
SetViewportExtEx(hdc,
pmfp->xExt * GetDeviceCaps(hdc, HORZRES) /
GetDeviceCaps(hdc, HORZSIZE) / 1000,
pmfp->yExt * GetDeviceCaps(hdc, VERTRES) /
GetDeviceCaps(hdc, VERTSIZE) / 1000,
NULL);
else if (pmfp->xExt < 0)
{
xScale = 100 * cxClient * GetDeviceCaps(hdc, HORZSIZE) /
GetDeviceCaps(hdc, HORZRES) / -pmfp->xExt;
yScale = 100 * cyClient * GetDeviceCaps(hdc, VERTSIZE) /
GetDeviceCaps(hdc, VERTRES) / -pmfp->yExt;
iScale = min(xScale, yScale);
SetViewportExtEx(hdc,
-pmfp->xExt * iScale * GetDeviceCaps(hdc, HORZRES) /
GetDeviceCaps(hdc, HORZSIZE) / 100,
-pmfp->yExt * iScale * GetDeviceCaps(hdc, VERTRES) /
GetDeviceCaps(hdc, VERTSIZE) / 100,
NULL);
}
}
}
这段代码假定 xExt 和 yExt 的值都可以是 0,也可以大于或者小于 0(应该是这种情况)。如果它们为 0,表示没有建议的尺寸或纵横比,此时视口范围被设置为要显示图元文件的区域。正的 xExt 与 yExt 表示建议的图像尺寸,以 0.01mm 为单位。GetDeviceCaps 函数用于确定每 0.01 mm 含有的像素的数目,然后该值乘以图元文件图片结构的范围值(xExt 和 yExt)。负的 xExt 及 yExt 表示建议的是纵横比而不是图像尺寸。iScale 变量(表示比例因子)的值是通过以 mm 为单位的 cxClient 和 cyClient 的纵横比计算出来的。然后,该比例因子被用于指定以像素为单位的视口范围。
完成以上任务后,就可以设置视口原点(如果需要),显示该图元文件,并返回到正常的设备环境:
PlayMetaFile (pMFP->hMF);
RestoreDC(hdc, -1);
然后应解除对内存块的锁定,并关闭剪贴板:
GlobalUnlock (hGlobal);
CloseClipboard();
如果程序使用的是增强型图元文件,就不需要作这项工作。如果一个应用程序将某种格式的图元文件放入剪贴板,但是另一个应用程序从剪贴板请求另一种格式时,Windows 剪贴板会自动地在老格式和增强型格式之间转换。