当前位置: 首页 > 文档资料 > Windows 程序设计 >

MetaFile

优质
小牛编辑
134浏览
2023-12-01

MetaFile和向量图形的关系,就像位图和位映像图形的关系一样。位图通常来自实际的图像,而MetaFile则大多是通过计算机程序人为建立的。MetaFile由一系列与图形函数呼叫相同的二进制记录组成,这些记录一般用于绘制直线、曲线、填入的区域和文字等。

「画图(paint)」程序建立位图,而「绘图(draw)」程序建立MetaFile。在优秀的绘图程序中,能轻易地「抓住」某个独立的图形对象(例如一条直线)并将它移动到其它位置。这是因为组成图形的每个成员都是以单独的记录储存的。在画图程序中,这是不可能的-您通常都会局限于删除或插入位图矩形块。

由于MetaFile以图形绘制命令描述图像,因此可以对图像进行缩放而不会失真。位图则不然,如果以二倍大小来显示位图,您却无法得到二倍的分辨率,而只是在水平和垂直方向上重复位图的位。

MetaFile可以转换为位图,但是会丢失一些信息:组成MetaFile的图形对象将不再是独立的,而是被合并进大的图像。将位图转换为MetaFile要艰难得多,一般仅限于非常简单的图像,而且它需要大量处理来分析边界和轮廓。而MetaFile可以包含绘制位图的命令。

虽然MetaFile可以作为图片剪辑储存在磁盘上,但是它们大多用于程序通过剪贴簿共享图片的情况。由于MetaFile将图片描述为图像函数呼叫的集合,因而它们既比位图占用更少的空间,又比位图更与设备无关。

MicrosoftWindows支持两种MetaFile格式和支持这些格式的两组函数。我首先讨论从Windows1.0到目前的32位Windows版本都支持的MetaFile函数,然后讨论为32位Windows系统开发的「增强型MetaFile」。增强型MetaFile在原有MetaFile的基础上有了一些改进,应该尽可能地加以利用。

旧的 MetaFile 格式

MetaFile既能够暂时储存在内存中,也能够以文件的形式储存在磁盘上。对应用程序来说,两者区别不大,尤其是由Windows来处理磁盘上储存和加载MetaFile资料的文件I/O时,更是如此。

内存MetaFile的简单利用

如果呼叫CreateMetaFile函数来建立MetaFile设备内容,Windows就会以早期的格式建立一个MetaFile,然后您可以使用大部分GDI绘图函数在该MetaFile设备内容上进行绘图。这些GDI呼叫并不在任何具体的设备上绘图,相反地,它们被储存在MetaFile中。当关闭MetaFile设备内容时,会得到MetaFile的句柄。这时就可以在某个具体的设备内容上「播放」这个MetaFile,这与直接执行MetaFile中GDI函数的效果等同。

CreateMetaFile只有一个参数,它可以是NULL或文件名称。如果是NULL,则MetaFile储存在内存中。如果是文件名称(以.WMF作为「WindowsMetaFile」的扩展名),则MetaFile储存在磁盘文件中。

程序18-1中的MetaFile显示了在WM_CREATE消息处理期间建立内存MetaFile的方法,并在WM_PAINT消息处理期间将图像显示100遍。

程序18-1 MetaFile
  
MetaFile.C  
/*-------------------------------------------------------------------------  
  MetaFile.C -- MetaFile Demonstration 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") ;   HWNDhwnd ;  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 ;   HDChdc, 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) ;  
}  

这个程序展示了在使用内存MetaFile时所涉及的4个MetaFile函数的用法。第一个是CreateMetaFile。在WM_CREATE消息处理期间用NULL参数呼叫该函数,并传回MetaFile设备内容的句柄。然后,MetaFile利用这个MetaFileDC来绘制两条直线和一个蓝色椭圆。这些函数呼叫以二进制形式储存在MetaFile中。CloseMetaFile函数传回MetaFile的句柄。因为以后还要用到该MetaFile句柄,所以把它储存在静态变量。

该MetaFile包含GDI函数呼叫的二进制表示码,它们是两个MoveToEx呼叫、两个LineTo呼叫、一个SelectObject呼叫(指定蓝色画刷)和一个Ellipse呼叫。坐标没有指定任何映像方式或转换,它们只是作为数值数据被储存在MetaFile中。

在WM_PAINT消息处理期间,MetaFile设定一种映像方式并呼叫PlayMetaFile在窗口中绘制对象100次。MetaFile中函数呼叫的坐标按照目的设备内容的目前变换方式加以解释。在呼叫PlayMetaFile时,事实上是在重复地呼叫最初在WM_CREATE消息处理期间建立MetaFile时,在CreateMetaFile和CloseMetaFile之间所做的所有呼叫。

和任何GDI对象一样,MetaFile对象也应该在程序终止前被删除。这是在WM_DESTROY消息处理期间用DeleteMetaFile函数处理的工作。

MetaFile程序的结果如图18-1所示。

图18-1 MetaFile程序执行结果显示

将MetaFile储存在磁盘上

在上面的例子中,CreateMetaFile的NULL参数表示要建立储存在内存中的MetaFile。我们也可以建立作为文件储存在磁盘上的MetaFile,这种方法对于大的MetaFile比较合适,因为可以节省内存空间。而另一方面,每次使用磁盘上的MetaFile时,就需要存取磁盘。

要把MetaFile转换为使用MetaFile磁盘文件的程序,必须把CreateMetaFile的NULL参数替换为文件名称。在WM_CREATE处理结束时,可以用MetaFile句柄来呼叫DeleteMetaFile,这样句柄被删除,但是磁盘文件仍然被储存着。

在处理WM_PAINT消息处理期间,可以通过呼叫GetMetaFile来取得此磁盘文件的MetaFile句柄:

hmf = GetMetaFile (szFileName) ;  

现在就可以像前面那样显示这个MetaFile。在WM_PAINT消息处理结束时,可以用下面的叙述删除该MetaFile句柄:

DeleteMetaFile (hmf) ;  

在开始处理WM_DESTROY消息时,不必删除MetaFile,因为它已经在WM_CREATE消息和每个WM_PAINT消息结束时被删除了,但是仍然需要删除磁盘文件:

DeleteFile (szFileName) ;  

当然,除非您想储存该文件。

正如在第十章讨论过的,MetaFile也可以作为使用者自订资源。您可以简单地把它当作数据块加载。如果您有一块包含MetaFile内容的资料,那么您可以使用

hmf = SetMetaFileBitsEx (iSize, pData) ;  

来建立MetaFile。SetMetaFileBitsEx有一个对应的函数-GetMetaFileBitsEx,此函数将MetaFile的内容复制到内存块中。

老式MetaFile与剪贴簿

老式MetaFile有个讨厌的缺陷。如果您具有老式MetaFile的句柄,那么,当您在显示MetaFile时如何确定它的大小呢?除非您深入分析MetaFile的内部结构,否则无法得知。

此外,当程序从剪贴簿取得老式MetaFile时,如果MetaFile被定义为在MM_ISOTROPIC或MM_ANISOTROPIC映像方式下显示,则此程序在使用该MetaFile时具有最大程度的灵活性。程序收到该MetaFile后,就可以在显示它之前简单地通过设定视埠的范围来缩放图像。然而,如果MetaFile内的映像方式被设定为MM_ISOTROPIC或MM_ANISOTROPIC,则收到该MetaFile的程序将无法继续执行。程序仅能在显示MetaFile之前或之后进行GDI呼叫,不允许在显示MetaFile当中进行GDI呼叫。

为了解决这些问题,老式MetaFile句柄不直接放入剪贴簿供其它程序取得,而是作为「MetaFile图片」(MetaFilePICT结构型态)的一部分。此结构使得从剪贴簿上取得MetaFile图片的程序能够在显示MetaFile之前设定映像方式和视埠范围。

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 to the MetaFile  
}  
MetaFilePICT ;  

对于MM_ISOTROPIC和MM_ANISOTROPIC以外的所有映像方式,图像大小用xExt和yExt值表示,其单位是由mm给出的映像方式的单位。利用这些信息,从剪贴簿复制MetaFile图片结构的程序就能够确定在显示MetaFile时所需的显示空间。建立该MetaFile的程序可以将这些值设定为输入MetaFile的GDI绘制函数中所使用的最大的x坐标和y坐标值。

在MM_ISOTROPIC和MM_ANISOTROPIC映射方式下,xExt和yExt字段有不同的功能。我们在第五章中曾介绍过一个程序,该程序为了在GDI函数中使用与图像实际尺寸无关的逻辑单位而采用MM_ISOTROPIC或MM_ANISOTROPIC映射方式。当程序只想保持纵横比而可以忽略图形显示平面的大小时,采用MM_ISOTROPIC模式;反之,当不需要考虑纵横比时采用MM_ANISOTROPIC模式。您也许还记得,第五章中在程序将映像方式设定为MM_ISOTROPIC或MM_ANISOTROPIC后,通常会呼叫SetWindowExtEx和SetViewportExtEx。SetWindowExtEx呼叫使用逻辑单位来指定程序在绘制时使用的单位,而SetViewportExtEx呼叫使用的设备单位大小则取决于图形显示平面(例如,窗口显示区域的大小)。

如果程序为剪贴簿建立了MM_ISOTROPIC或MM_ANISOTROPIC方式的MetaFile,则该MetaFile本身不应包含对SetViewportExtEx的呼叫,因为该呼叫中的设备单位应该依据建立MetaFile的程序的显示平面,而不是依据从剪贴簿读取并显示MetaFile的程序的显示平面。从剪贴簿取得MetaFile的程序可以利用xExt和yExt值来设定合适的视埠范围以便显示MetaFile。但是当映像方式是MM_ISOTROPIC或MM_ANISOTROPIC时,MetaFile本身包含设定窗口范围的呼叫。MetaFile内的GDI绘图函数的坐标依据这些窗口的范围。

建立MetaFile和MetaFile图片遵循以下规则:

  • 设定MetaFilePICT结构的mm字段来指定映像方式。
  • 对于MM_ISOTROPIC和MM_ANISOTROPIC以外的映像方式,xExt与yExt字段设定为图像的宽和高,单位与mm字段相对应。对于在MM_ISOTROPIC或MM_ANISOTROPIC方式下显示的MetaFile,工作要复杂一些。在MM_ANISOTROPIC模式下,当程序既不对图片大小跟纵横比给出任何建议信息时,xExt和yExt的值均为零。在这两种模式下,如果xExt和yExt的值为正数,它们就是以0.01mm单位(MM_HIMETRIC单位)表示该图像的宽度和高度。在MM_ISOTROPIC方式下,如果xExt和yExt为负值,它们就指出了图像的纵横比而不是大小。
  • 在MM_ISOTROPIC和MM_ANISOTROPIC映像方式下,MetaFile本身含有对SetWindowExtEx的呼叫,也可能有对SetWindowOrgEx的呼叫。亦即,建立MetaFile的程序在MetaFile设备内容中呼叫这些函数。MetaFile一般不会包含对SetMapMode、SetViewportExtEx或SetViewportOrgEx的呼叫。
  • MetaFile应该是内存MetaFile,而不是MetaFile文件。

这里有一段范例程序代码,它建立MetaFile并将其复制到剪贴簿。如果MetaFile使用MM_ISOTROPIC或MM_ANISOTROPIC映像方式,则该MetaFile的第一个呼叫应该设定窗口范围(在其它模式中,窗口的大小是固定的)。无论在哪种模式下,窗口的位置应如下设定:

hdcMeta = CreateMetaFile (NULL) ;  
SetWindowExtEx (hdcMeta, ...) ;  
SetWindowOrgEx (hdcMeta, ...) ;  

MetaFile绘图函数中的坐标决定于这些窗口范围和窗口原点。当程序使用GDI呼叫在MetaFile设备内容中绘制完成后,关闭MetaFile以得到MetaFile句柄:

hmf = CloseMetaFile (hdcMeta) ;  

该程序还需要定义指向MetaFilePICT型态结构的指针,并为此结构配置一块整体内存:

GLOBALHANDLE   hGlobal ;  
LPMetaFilePICT pMFP ;  
其它行程序  
hGlobal= GlobalAlloc (GHND | GMEM_SHARE, sizeof (MetaFilePICT)) ;  
pMFP = (LPMetaFilePICT) GlobalLock (hGlobal) ;  

接着,程序设定该结构的4个字段:

pMFP->mm   = MM_... ;  
pMFP->xExt = ...  ;  
pMFP->yExt = ...  ;  
pMFP->hMF  = hmf  ;  
GlobalUnlock (hGlobal) ;  

然后,程序将包含有MetaFile图片的整体内存块传送给剪贴簿:

OpenClipboard (hwnd) ;  
EmptyClipboard () ;  
SetClipboardData (CF_MetaFilePICT, hGlobal) ;  
CloseClipboard () ;  

完成这些呼叫后,hGlobal句柄(包含MetaFile图片结构的内存块)和hmf句柄(MetaFile本身)就对建立它们的程序失效了。

现在来看一看难的部分。当程序从剪贴簿取得MetaFile并显示它时,必须完成下列步骤:

  1. 程序利用MetaFile图片结构的mm字段设定映像方式。
  2. 对于MM_ISOTROPIC或MM_ANISOTROPIC以外的映像方式,程序用xExt和yExt值设定剪贴矩形或简单地设定图像大小。而在MM_ISOTROPIC和MM_ANISOTROPIC映像方式,程序使用xExt和yExt来设定视埠范围。
  3. 然后,程序显示MetaFile。

下面程序代码,首先打开剪贴簿,得到MetaFile图片结构句柄并将其锁定:

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分别表示MetaFile显示区域的图素高度和宽度。

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) / 100),  pmfp->yExt *  GetDeviceCaps (hdc, VERTRES) /   GetDeviceCaps (hdc, VERTSIZE) / 100), NULL) ; else if (pmfp->xExt < 0)  { xScale = 100 *cxClient * GetDeviceCaps (hdc, HORZSIZE) /   GetDeviceCaps (hdc, HORZRES) / -pmfp->xExt ;  lScale = 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同时都为零、大于零或小于零,这三种状态之一。如果范围为零,表示没有建议大小或纵横比,视埠范围设定为显示MetaFile的区域。如果大于零,则xExt和yExt的值代表图像的建议大小,单位是0.01mm。GetDeviceCaps函数用来确定每0.01mm中包含的图素数,并且该值与MetaFile图片结构的范围值相乘。如果小于零,则xExt和yExt的值表示建议的纵横比而不是建议的大小。iScale的值首先根据对应cxClient和cyClient的毫米表示的纵横比计算出来,该缩放因子用于设定图素单位的视端口范围。

完成了上述工作后,可以设定视埠原点,显示MetaFile,并恢复设备内容:

PlayMetaFile (pMFP->hMF) ;  
RestoreDC (hdc, -1) ;  

然后,对内存块解锁并关闭剪贴簿:

GlobalUnlock (hGlobal) ;  
CloseClipboard () ;  

如果程序使用增强型MetaFile就可以省去这项工作。当某个应用程序将这些格式放入剪贴簿而另一个程序却要求从剪贴簿中获得其它格式时,Windows剪贴簿会自动在老式MetaFile和增强型MetaFile之间进行格式转换。

增强型 MetaFile

「增强型MetaFile」格式是在32位Windows版本中发表的。它包含一组新的函数呼叫、一对新的数据结构、新的剪贴簿格式和新的文件扩展名.EMF。

这种新的MetaFile格式最重要的改进是加入可通过函数呼叫取得的更丰富的表头信息,这种表头信息可用来帮助应用程序显示MetaFile图像。

有些增强型MetaFile函数使您能够在增强型MetaFile(EMF)格式和老式MetaFile格式(也称作WindowsMetaFile(WMF)格式)之间来回转换。当然,这种转换很可能遇到麻烦,因为老式MetaFile格式并不支持某些,例如GDI绘图路径等,新的32位图形功能。

基本程序

程序18-2所示的EMF1建立并显示增强型MetaFile。

程序18-2 EMF1
EMF1.C  
/*----------------------------------------------------------------------------  
  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,LPSTR lpszCmdLine, int nCmdShow)  
{   static TCHAR szAppName[] = TEXT ("EMF1") ;   HWNDhwnd ;   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   = 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, nCmdShow) ;   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 ;   RECTrect ;  
  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来建立增强型MetaFile。该函数有4个参数,但可以把它们都设为NULL。稍候我将说明这4个参数在非NULL情况下的使用方法。

和CreateMetaFile一样,CreateEnhMetaFile函数传回特定的设备内容句柄。该程序利用这个句柄绘制一个矩形和该矩形的两条对角线。这些函数呼叫及其参数被转换为二进制元的形式并储存在MetaFile中。

最后通过对CloseEnhMetaFile函数的呼叫结束了增强型MetaFile的建立并传回指向它的句柄。该文件句柄储存在HENHMetaFile型态的静态变量中。

在WM_PAINT消息处理期间,EMF1以RECT结构取得程序的显示区域窗口大小。通过调整结构中的4个字段,使该矩形的长和宽为显示区域窗口长和宽的一半并位于窗口的中央。然后EMF1呼叫PlayEnhMetaFile,该函数的第一个参数是窗口的设备内容句柄,第二个参数是该增强型MetaFile的句柄,第三个参数是指向RECT结构的指针。

在MetaFile的建立程序中,GDI得出整个MetaFile图像的尺寸。在本例中,图像的长和宽均为100个单位。在MetaFile的显示程序中,GDI将图像拉伸以适应PlayEnhMetaFile函数指定的矩形大小。EMF1在Windows下执行的三个执行实体如图18-2所示。

图18-2 EMF1得屏幕显示

最后,在WM_DESTROY消息处理期间,EMF1呼叫DeleteEnhMetaFile删除MetaFile。

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

首先,该程序在建立增强型MetaFile时,画矩形和直线的函数所使用的坐标并不是实际意义上的坐标。您可以将它们同时加倍或都减去某个常数,而其结果不会改变。这些坐标只是在定义图像时说明彼此间的对应关系。

其次,为了适于在传递给PlayEnhMetaFile函数的矩形中显示,图像大小会被缩放。因此,如图18-2所示,图像可能会变形。尽管MetaFile坐标指出该图像是正方形的,但一般情况下我们却得不到这样的图像。而在某些时候,这又正是我们想要得到的图像。例如,将图像嵌入一段文书处理格式的文字中时,可能会要求使用者为图像指定矩形,并且确保整个图像恰好位于矩形中而不浪费空间。这样,使用者可通过适当调整矩形的大小来得到正确的纵横比。

然而有时候,您也许希望保留图像最初的纵横比,因为这一点对于表现视觉信息尤为重要。例如,警察的嫌疑犯草图既不能比原型胖也不能比原型瘦。或者您希望保留原来图像的度量尺寸,图像必须是两英…几撸裨蚓筒荒苷O允尽T谡庵智榭鱿拢A敉枷竦脑闯叽缇头浅V匾恕?/p>

同时也要注意MetaFile中画出的那些对角线似乎没有与矩形顶点相交。这是由于Windows在MetaFile中储存矩形坐标的方式造成的。稍后,会说明解决这个问题的方法。

揭开内幕

如果看一看MetaFile的内容会对MetaFile工作的方式有一个更好的理解。如果您有一个MetaFile文件,这将很容易做到,程序18-3中的EMF2程序建立了一个MetaFile。

程序18-3 EMF2
EMF2.C  
/*-----------------------------------------------------------------------------  
  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,LPSTR lpszCmdLine, int nCmdShow)  
{   static TCHAR  szAppName[] = TEXT ("EMF2") ;   HWNDhwnd ;   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   = 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, nCmdShow) ;   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) ;  
}  

在EMF1程序中,CreateEnhMetaFile函数的所有参数均被设定为NULL。在EMF2中,第一个参数仍旧设定为NULL,该参数还可以是设备内容句柄。GDI使用该参数在MetaFile表头中插入度量信息,很快我会讨论它。如果该参数为NULL,则GDI认为度量信息是由视讯设备内容决定的。

CreateEnhMetaFile函数的第二个参数是文件名称。如果该参数为NULL(在EMF1中为NULL,但在EMF2中不为NULL),则该函数建立内存MetaFile。EMF2建立名为EMF2.EMF的MetaFile文件。

函数的第三个参数是RECT结构的地址,它指出了以0.01mm为单位的MetaFile的总大小。这是MetaFile表头数据中极其重要的信息(这是早期的WindowsMetaFile格式的缺陷之一)。如果该参数为NULL,GDI会计算出尺寸。我比较喜欢让操作系统替我做这些事,所以将该参数设定为NULL。当应用程序对性能要求比较严格时,就需要使用该参数以避免让GDI处理太多东西。

最后的参数是描述该MetaFile的字符串。该字符串分为两部分:第一部分是以NULL字符结尾的应用程序名称(不一定是程序的文件名称),第二部分是描述视觉图像内容的说明,以两个NULL字符结尾。例如用C中的符号「\0」作为NULL字符,则该描述字符串可以是「LoonyCadV6.4\0FlyingFrogs\0\0」。由于在C中通常会在使用的字符串末尾放入一个NULL字符,所以如EMF2所示,在末尾仅需一个「\0」。

建立完MetaFile后,与EMF1一样,EMF2也透过利用由CreateEnhMetaFile函数传回的设备内容句柄进行一些GDI函数呼叫。然后程序呼叫CloseEnhMetaFile删除设备内容句柄并取得完成的MetaFile的句柄。

然后,在WM_CREATE消息还没处理完毕时,EMF2做了一些EMF1没有做的事情:在获得MetaFile句柄之后,程序呼叫DeleteEnhMetaFile。该操作释放了用于储存MetaFile的所有内存资源。然而,MetaFile文件仍然保留在磁盘驱动器中(如果愿意,您可以使用如DeleteFile的文件删除函数来删除该文件)。注意MetaFile句柄并不像EMF1中那样储存在静态变量中,这意味着在消息之间不需要储存它。

现在,为了使用该MetaFile,EMF2需要存取磁盘文件。这是在WM_PAINT消息处理期间透过呼叫GetEnhMetaFile进行的。MetaFile的文件名称是该函数的唯一参数,该函数传回MetaFile句柄。和EMF1一样,EMF2将这个文件句柄传递给PlayEnhMetaFile函数。该MetaFile图像在PlayEnhMetaFile函数的最后一个参数所指定的矩形中显示。与EMF1不同的是,EMF2在WM_PAINT消息结束之前就删除该MetaFile。此后每次处理WM_PAINT消息时,EMF2都会再次读取MetaFile,显示并删除它。

要记住,对MetaFile的删除操作仅是释放了用以储存MetaFile的内存资源而已,磁盘MetaFile甚至在程序执行结束后还保留在磁盘上。

由于EMF2留下了MetaFile文件,您可以看一看它的内容。图18-3显示了该程序建立的EMF2.EMF文件的一堆十六进制代码。

000001 00 00 00 88 00 00 00 64 00 00 00 64 00 00 00 ........d...d...  
0010C8 00 00 00 C8 00 00 00 35 0C 00 00 35 0C 00 00  ........5...5...  
00206A 18 00 00 6A 18 00 00 20 45 4D 46 00 00 01 00  j...j...EMF....  
0030F4 00 00 00 07 00 00 00 01 00 00 00 12 00 00 00  ................  
004064 00 00 00 00 00 00 00 00 04 00 00 00 03 00 00  d...............  
005040 01 00 00 F0 00 00 00 00 00 00 00 00 00 00 00  @...............  
006000 00 00 00 45 00 4D 00 46 00 32 00 00 00 45 00  ....E.M.F.2...E.  
00704D 00 46 00 20 00 44 00 65 00 6D 00 6F 00 20 00  M.F..D.e.m.o..  
008023 00 32 00 00 00 00 00 2B 00 00 00 18 00 00 00  #.2.....+.......  
009063 00 00 00 63 00 00 00 C6 00 00 00 C6 00 00 00  c...c...........  
00A01B 00 00 00 10 00 00 00 64 00 00 00 64 00 00 00  ........d...d...  
00B036 00 00 00 10 00 00 00 C8 00 00 00 C8 00 00 00  6...............  
00C01B 00 00 00 10 00 00 00 C8 00 00 00 64 00 00 00  ............d...  
00D036 00 00 00 10 00 00 00 64 00 00 00 C8 00 00 00  6.......d.......  
00E00E 00 00 00 14 00 00 00 00 00 00 00 10 00 00 00  ................  
00F014 00 00 00 ....  

图18-3 EMF2.EMF的十六进制代码

图18-3所示的MetaFile是EMF2在Microsoft Windows NT4下,视讯显示器的分辨率为1024×768时建立的。同一程序在Windows98下建立的MetaFile会比前者少12个字节,这一点将在稍后讨论。同样地,视讯显示器的分辨率也影响MetaFile表头的某些信息。

增强型MetaFile格式使我们对MetaFile的工作方式有更深刻的理解。增强型MetaFile由可变长度的记录组成,这些记录的一般格式由ENHMETARECORD结构说明,它在WINGDI.H表头文件中定义如下:

typedef struct tagENHMETARECORD  
{  DWORD iType ;// record type   DWORD nSize ;// record size   DWORD dParm [1] ;// parameters  
}  
ENHMETARECORD ;  

当然,那个只有一个元素的数组指出了数组元素的变量。参数的数量取决于记录型态。iType字段可以是定义在WINGDI.H文件中以前缀EMR_开始的近百个常数之一。nSize字段是总记录的大小,包括iType和nSize字段以及一个或多个dParm字段。

有了这些知识后,让我们看一下图18-3。第一个字段型态为0x00000001,大小为0x00000088,所以它占据文件的前136个字节。记录型态为1表示常数EMR_HEADER。我们不妨把对表头纪录的讨论往后搁,先跳到位于第一个记录末尾的偏移量0x0088处。

后面的5个记录与EMF2建立MetaFile之后的5个GDI函数呼叫有关。该记录在偏移量0x0088处有一个值为0x0000002B的型态代码,这代表EMR_RECTANGLE,很明显是用于Rectangle呼叫的MetaFile记录。它的长度为0x00000018(十进制24)字节,用以容纳4个32位参数。实际上Rectangle函数有5个参数,但是第一个参数,也就是设备内容句柄并未储存在MetaFile中,因为它没有实际意义。尽管在EMF2的函数呼叫中指定了矩形的顶点坐标分别是(100,100)和(200,200),但4个参数中的2个是0x00000063(99),另外2个是0x000000C6 (198)。EMF2程序在Windows98下建立的MetaFile显示出前两个参数是0x00000064(100),后2个参数是0x000000C7(199)。显然,在Rectangle参数储存到MetaFile之前,Windows对它们作了调整,但没有保持一致。这就是对角线端点与矩形顶点不能重合的原因。

其次,有4个16位记录与2个MoveToEx(0x0000001B或EMR_MOVETOEX)和LineTo(0x00000036或EMR_LINETO)呼叫有关。位于MetaFile中的参数与传递给函数的参数相同。

MetaFile以20个字节长的型态代码为0x0000000E或EMR_EOF(「end offile」)的记录结尾。

增强型MetaFile总是以表头纪录开始。它对应于ENHMETAHEADER型态的结构,定义如下:

typedef struct tagENHMETAHEADER  
{   DWORD iType ;   // EMR_HEADER = 1   DWORD nSize ;   // structure size   RECTL rclBounds ;// bounding rectangle in pixels   RECTL rclFrame ; // size of image in 0.01 millimeters   DWORD dSignature ;  // ENHMETA_SIGNATURE = " EMF"   DWORD nVersion ;// 0x00010000   DWORD nBytes ; // file size in bytes   DWORD nRecords ;   // total number of records WORD  nHandles ;   // number of handles in handle table   WORD  sReserved ;   DWORD nDescription ;// character length of description string   DWORD offDescription ;   // offset of description string in file   DWORD nPalEntries ; // number of entries in palette   SIZEL szlDevice ;// device resolution in pixels   SIZEL szlMillimeters ;  // device resolution in millimeters   DWORD cbPixelFormat ;  // size of pixel format   DWORD offPixelFormat ;  // offset of pixel format   DWORD bOpenGL ;  // FALSE if no OpenGL records  
}  
ENHMETAHEADER ;  

这种表头纪录的存在可能是增强型MetaFile格式对早期WindowsMetaFile所做的最为重要的改进。不需要对MetaFile文件使用文件I/O函数来取得这些表头信息。如果具有MetaFile句柄,就可以使用GetEnhMetaFileHeader函数:

GetEnhMetaFileHeader (hemf, cbSize, &emh) ;  

第一个参数是MetaFile句柄。最后一个参数是指向ENHMETAHEADER结构的指针。第二个参数是该结构的大小。可以使用类似的GetEnh-MetaFileDescription函数取得描述字符串。

如上面所定义的,ENHMETAHEADER结构有100字节长,但在MF2.EMFMetaFile中,记录的大小包括描述字符串,所以大小为0x88,即136字节。而Windows98MetaFile的表头纪录不包含ENHMETAHEADER结构的最后3个字段,这一点解释了12个字节的差别。

rclBounds字段是指出图像大小的RECT结构,单位是图素。将其从十六进制转换过来,我们看到该图像正如我们希望的那样,其左上角位于(100,100),右下角位于(200,200)。

rclFrame字段是提供相同信息的另一个矩形结构,但它是以0.01毫米为单位。在这种情况下,该文件显示两对角顶点分别位于(0x0C35,0x0C35)和(0x186A,0x186A),用十进制表示为(3125,3125)和(6250,6250)的矩形。这些数字是怎么来的?我们很快就会明白。

dSignature字段始终为值ENHMETA_SIGNATURE或0x464D4520。这看上去是一个奇怪的数字,但如果将字节的排列顺序倒过来(就像Intel处理器在内存中储存多字节数那样)并转换成ASCII码,就变成字符串"EMF"。dVersion字段的值始终是0x00010000。

其后是nBytes字段,该字段在本例中是0x000000F4,这是该MetaFile的总字节数。nRecords字段(在本例中是0x00000007)指出了记录数-包括表头纪录、5个GDI函数呼叫和文件结束记录。

下面是两个十六位的字段。nHandles字段为0x0001。该字段一般指出MetaFile所使用的图形对象(如画笔、画刷和字体)的非内定句柄的数量。由于没有使用这些图形对象,您可能会认为该字段为零,但实际上GDI自己保留了第一个字段。我们将很快见到句柄储存在MetaFile中的方式。

下两个字段指出描述字符串的字符个数,以及描述字符串在文件中的偏移量,这里它们分别为0x00000012(十进制数18)和0x00000064。如果MetaFile没有描述字符串,则这两个字段均为零。

nPalEntries字段指出在MetaFile的调色盘表中条目的个数,本例中没有这种情况。

接着表头纪录包括两个SIZEL结构,它们包含两个32位字段,cx和cy。szlDevice字段(在MetaFile中的偏移量为0x0040)指出了以图素为单位的输出设备大小,szlMillimeters字段(偏移量为0x0050)指出了以毫米为单位的输出设备大小。在增强型MetaFile文件中,这个输出设备被称作「参考设备(referencedevice)」。它是依据作为第一个参数传递给CreateEnhMetaFile呼叫的句柄所指出的设备内容。如果该参数设为NULL,则GDI使用视讯显示器。当EMF2建立上面所示的MetaFile时,正巧是在WindowsNT上以1024×768显示模式工作,因此这就是GDI使用的参考设备。

GDI通过呼叫GetDeviceCaps取得此信息。EMF2.EMF中的szlDevice字段是0x0400×0x0300(即1024×768),它是以HORZRES和VERTRES作为参数呼叫GetDeviceCaps得到的。szlMillimeters字段是0x140×0xF0,或320×240,是以HORZSIZE和VERTSIZE作为参数呼叫GetDeviceCaps得到的。

通过简单的除法就可以得出图素为0.3125mm高和0.3125mm宽,这就是前面描述的GDI计算rclFrame矩形尺寸的方法。

在MetaFile中,ENHMETAHEADER结构后跟一个描述字符串,该字符串是CreateEnhMetaFile函数的最后一个参数。在本例中,该字符串由后跟一个NULL字符的「EMF2」字符串和后跟两个NULL字符的「EMFDemo#2」字符串组成。总共18个字符,要是以Unicode方式储存则为36个字符。无论建立MetaFile的程序执行在WindowsNT还是Windows 98下,该字符串始终以Unicode方式储存。

MetaFile与GDI物件

我们已经知道了GDI绘图命令储存在MetaFile中方式,现在看一下GDI对象的储存方式。程序18-4EMF3除了建立用于绘制矩形和直线的非内定画笔和画刷以外,与前面介绍的EMF2程序很相似。该程序也对Rectangle坐标的问题提出了一点修改。EMF3程序使用GetVersion来确定执行环境是Windows98还是Windows NT,并适当地调整参数。

程序18-4 EMF3
EMF3.C  
/*----------------------------------------------------------------------------  
  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  = 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   // Windows NT 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函数时,这些函数呼叫被储存在MetaFile中而不是直接输出到屏幕或打印机上。然而,一些GDI函数根本不涉及特定的设备内容。其中有关建立画笔和画刷等图形对象的GDI函数十分重要。虽然逻辑画笔和画刷的定义储存在由GDI保留的内存中,但是在建立这些对象时,这些抽象的定义并未与任何特定的设备内容相关。

EMF3呼叫CreateSolidBrush和ExtCreatePen函数。因为这些函数不需要设备内容句柄,`所以GDI不会把这些呼叫储存在MetaFile里。当呼叫它们时,GDI函数只是简单地建立图形绘制对象而不会影响MetaFile。

然而,当程序呼叫SelectObject函数将GDI对象选入MetaFile设备内容时,GDI既为对象建立函数编码(源自用于储存对象的内部GDI数据)也为MetaFile中的SelectObject呼叫进行编码。为了解其工作方式,我们来看一下EMF3.EMF文件的十六进制代码,如图18-4所示:

000001 00 00 00 88 00 00 00 60 00 00 00 60 00 00 00 ........`...`...  
0010CC 00 00 00 CC 00 00 00 B8 0B 00 00 B8 0B 00 00 ................  
0020E7 18 00 00 E7 18 00 00 20 45 4D 46 00 00 01 00  ........EMF.....  
003088 01 00 00 0F 00 00 00 03 00 00 00 12 00 00 00  ................  
004064 00 00 00 00 00 00 00 00 04 00 00 00 03 00 00  d...............  
005040 01 00 00 F0 00 00 00 00 00 00 00 00 00 00 00  @...............  
006000 00 00 00 45 00 4D 00 46 00 33 00 00 00 45 00  ....E.M.F.3...E.  
00704D 00 46 00 20 00 44 00 65 00 6D 00 6F 00 20 00  M.F....D.e.m.o..  
008023 00 33 00 00 00 00 00 27 00 00 00 18 00 00 00  #.3.....'.......  
009001 00 00 00 00 00 00 00 00 00 FF 00 00 00 00 00  ................  
00A025 00 00 00 0C 00 00 00 01 00 00 00 5F 00 00 00  %..........._...  
00B034 00 00 00 02 00 00 00 34 00 00 00 00 00 00 00  4.......4.......  
00C034 00 00 00 00 00 00 00 00 00 01 00 05 00 00 00  4...............  
00D000 00 00 00 FF 00 00 00 00 00 00 00 00 00 00 00  ................  
00E025 00 00 00 0C 00 00 00 02 00 00 00 2B 00 00 00  %...........+...  
00F018 00 00 00 63 00 00 00 63 00 00 00 C6 00 00 00  ....c...c.......  
0100C6 00 00 00 1B 00 00 00 10 00 00 00 64 00 00 00  ............d...  
011064 00 00 00 36 00 00 00 10 00 00 00 C8 00 00 00  d...6...........  
0120C8 00 00 00 1B 00 00 00 10 00 00 00 C8 00 00 00  ................  
013064 00 00 00 36 00 00 00 10 00 00 00 64 00 00 00  d...6.......d...  
0140C8 00 00 00 25 00 00 00 0C 00 00 00 07 00 00 80  ....%...........  
015028 00 00 00 0C 00 00 00 02 00 00 00 25 00 00 00  (...........%...  
01600C 00 00 00 00 00 00 80 28 00 00 00 0C 00 00 00  ........(.......  
017001 00 00 00 0E 00 00 00 14 00 00 00 00 00 00 00  ................  
018010 00 00 00 14 00 00 00........  

图18-4 EMF3.EMF的十六进制代码

如果把这个MetaFile跟前面的EMF2.EMF文件进行比较,第一个不同点就是EMF3.EMF表头部分中的rclBounds字段。在EMF2.EMF中,它指出图像限定在坐标(0x64,0x64)和(0xC8,0xC8)区域内。而在EMF3.EMF中,坐标是(0x60,0x60)和(0xCC,0xCC)。这表示使用了较粗的笔。rclFrame字段(以0.01mm为单位指出图像大小)也受到影响。

EMF2.EMF中的nBytes字段(偏移量为0x0030)显示该MetaFile长度为0xFA字节,EMF3.EMF中长度为0x0188字节。EMF2.EMFMetaFile包含7个记录(一个表头纪录,5个GDI函数呼叫和一个文件结束记录),但是EMF3.EMF文件包含15个记录。多出的8个记录是两个对象建立函数、4个对SelectObject函数的呼叫和两个对DeleteObject函数的呼叫。

nHandles字段(在文件中偏移量为0x0038)指出GDI对象的句柄个数。该字段的值总是比MetaFile使用的非内定对象数多一。(PlatformSDK文件解释这个多出来的一是「此表中保留的零索引」)。该字段在EMF2.EMF的值为1,而在EMF3.EMF中的值为3,多出的数指出了画笔和画刷。

让我们跳到文件中偏移量为0x0088的地方,即第二个记录(表头纪录之后的第一个记录)。记录型态为0x27,对应常数为EMR_CREATE-BRUSHINDIRECT。该MetaFile记录用于CreateBrushIndirect函数,此函数需要指向LOGBRUSH结构的指针作为参数。该记录的长度为0x18(或24)字节。

每个被选入MetaFile设备内容的非备用GDI对象得到一个号码,该号码从1开始编号。这在此记录的下4个字节中指出,在MetaFile中的偏移量是0x0090。此记录下面的3个4字节字段分别对应LOGBRUSH结构的3个字段:0x00000000(BS_SOLID的lbStyle字段)、0x00FF0000(lbColor字段)和0x00000000(lbHatch字段)。

下一个记录在EMF3.EMF中的偏移量为0x00A0,记录型态为0x25,或EMR_SELECTOBJECT,是用于SelectObject呼叫的MetaFile记录。该记录的长度为0x0C(或12)字节,下一个字段是数值0x01,指出它是选中的第一个GDI对象,这就是逻辑画刷。

EMF3.EMF中的偏移量0x00AC是下一个记录,它的记录型态为0x5F或EMR_EXTCREATEPEN。该记录有0x34(或52)个字节。下一个4字节字段是0x02,它表示这是在MetaFile内使用的第二个非备用GDI对象。

EMR_EXTCREATEPEN记录的下4个字段重复记录大小两次,之间用0字段隔开:0x34、0x00、0x34和0x00。下一个字段是0x00010000,它是PS_SOLID(0x00000000)与PS_GEOMETRIC(0x00010000)组合的画笔样式。接下来是5个单元的宽度,紧接着是ExtCreatePen中使用的逻辑画刷结构的3个字段,后接0字段。

如果建立了自订的扩展画笔样式,EMR_EXTCREATEPEN记录会超过52个字节,这样会影响记录的第二字段及两个重复的大小字段。在描述LOGBRUSH结构的3个字段后面不会是0(像在EMF3.EMF中那样),而是指出了虚线和空格的数量。这后面接着用于虚线和空格长度的许多字段。

EMF3.EMF的下一个12字节的字段是指出第二个对象(画笔)的另一个SelectObject呼叫。接下来的5个记录与EMF2.EMF中的一样-一个0x2B(EMR_RECTANGLE)的记录型态和两组0x1B(EMR_MOVETOEX)和0x36 (EMR_LINETO)记录。

这些绘图函数后面跟着两组0x25(EMR_SELECTOBJECT)和0x28(EMR_DELETEOBJECT)的12字节记录。选择对象记录具有0x80000007和0x80000000的参数。在设定高位时,它指出一个备用对象,在此例中是0x07(对应BLACK_PEN)和0x00(WHITE_BRUSH)。

DeleteObject呼叫有2和1两个参数,用于在MetaFile中使用的两个非内定对象。虽然DeleteObject函数并不需要设备内容句柄作为它的第一个参数,但GDI显然保留了MetaFile中使用的被程序删除的对象。

最后,MetaFile以0x0E(EMF_EOF)记录结束。

总结一下,每当非内定的GDI对象首次被选入MetaFile设备内容时,GDI都会为该对象建立函数的记录编码(此例中,为EMR_CREATEBRUSHINDIRECT和EMR_EXTCREATEPEN)。每个对象有一个依序从1开始的唯一数值,此数值由记录的第三个字段表示。跟在此记录后的是引用该数值的EMR_SELECTOBJECT记录。以后,将对象选入MetaFile设备内容时(在中间时期没有被删除),就只需要EMR_SELECTOBJECT记录了。

MetaFile和位图

现在,让我们做点稍微复杂的事,在MetaFile设备内容中绘制一幅位图,如程序18-5EMF4所示。

程序18-5 EMF4
EMF4.C  
/*----------------------------------------------------------------------------  
  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   = 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)  
{   BITMAPbm ;   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建立与目的设备内容(此例为MetaFile设备内容)兼容的内存设备内容。然后,通过使用SelectObject将位图选入该内存设备内容并且从该内存设备内容呼叫BitBlt或StretchBlt把位图画到目的设备内容。结束后,删除内存设备内容和位图。

您会注意到EMF4也呼叫GetObject来确定位图的大小。这对SelectObject呼叫是很必要的。

首先,这份程序代码储存MetaFile的空间对GDI来说就是个挑战。在StretchBlt呼叫前根本没有别的GDI函数去处理MetaFile的设备内容。因此,让我们来看一看EMF4.EMF里头是如何做的,图18-5只显示了一部分。

000001 00 00 00 88 00 00 00 64 00 00 00 64 00 00 00 ........d...d...  
0010C7 00 00 00 C7 00 00 00 35 0C 00 00 35 0C 00 00 ........5...5...  
00204B 18 00 00 4B 18 00 00 20 45 4D 46 00 00 01 00 K...K....EMF....  
0030F0 0E 00 00 03 00 00 00 01 00 00 00 12 00 00 00 ................  
004064 00 00 00 00 00 00 00 00 04 00 00 00 03 00 00 d...............  
005040 01 00 00 F0 00 00 00 00 00 00 00 00 00 00 00 @...............  
006000 00 00 00 45 00 4D 00 46 00 34 00 00 00 45 00 ....E.M.F.4...E.  
00704D 00 46 00 20 00 44 00 65 00 6D 00 6F 00 20 00 M.F...D.e.m.o...  
008023 00 34 00 00 00 00 00 4D 00 00 00 54 0E 00 00 #.4.....M...T...  
009064 00 00 00 64 00 00 00 C7 00 00 00 C7 00 00 00 d...d...........  
00A064 00 00 00 64 00 00 00 64 00 00 00 64 00 00 00 d...d...d...d...  
00B020 00 CC 00 00 00 00 00 00 00 00 00 00 00 80 3F ..............?  
00C000 00 00 00 00 00 00 00 00 00 80 3F 00 00 00 00 ...........?....  
00D000 00 00 00 FF FF FF 00 00 00 00 00 6C 00 00 00 ............l...  
00E028 00 00 00 94 00 00 00 C0 0D 00 00 28 00 00 00 (...........(...  
00F016 00 00 00 28 00 00 00 28 00 00 00 16 00 00 00 ....(...(.......  
010001 00 20 00 00 00 00 00 C0 0D 00 00 00 00 00 00 .. .............  
011000 00 00 00 00 00 00 00 00 00 00 00 C0 C0 C0 00 ................  
0120C0 C0 C0 00 C0 C0 C0 00 C0 C0 C0 00 C0 C0 C0 00 ................  
. . . .  
0ED0C0 C0 C0 00 C0 C0 C0 00 C0 C0 C0 00 0E 00 00 00 ................  
0EE014 00 00 00 00 00 00 00 10 00 00 00 14 00 00 00 ................  

图18-5 EMF4.EMF的部分十六进制代码

此MetaFile只包含3个记录-表头纪录、0x0E54位组长度的0x4D(或EMR_STRETCHBLT)和文件结束记录。

我不解释该记录每个字段的含义,但我会指出关键部分,以便理解GDI把EMF4.C中的一系列函数呼叫转化为单个MetaFile记录的方法。

GDI已经把原始的与设备相关的位图转化为与设备无关的位图(DIB)。整个DIB储存在记录着自身大小的记录中。我想,在显示MetaFile和位图时,GDI实际上使用StretchDIBits函数而不是StretchBlt。或者,GDI使用CreateDIBitmap把DIB转变回与设备相关的位图,然后使用内存设备内容及StretchBlt来显示位图。

EMR_STRETCHBLT记录开始于MetaFile的偏移量0x0088处。DIB储存在MetaFile中,以偏移量0x00F4开始,到0x0EDC处的记录结尾结束。DIB以BITMAPINFOHEADER型态的40字节的结构开始。在偏移量0x011C处接有22个图素行,每行40个图素。这是每图素32位的DIB,所以每个图素需要4个字节。

列举MetaFile内容

当您希望存取MetaFile内的个别记录时,可以使用称作MetaFile列举的程序。如程序18-6EMF5所示。此程序使用MetaFile来显示与EMF3相同的图像,但它是通过MetaFile列举来进行的。

程序18-6 EMF5
EMF5.C  
/*----------------------------------------------------------------------------  
  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   = 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 #5"), 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) ;  
}  

此程序使用EMF3程序建立的EMF3.EMF文件,所以确定在执行此程序前先执行EMF3程序。同时,需要在VisualC++环境中执行两个程序,以确保路径的正确。在处理WM_PAINT时,两个程序的主要区别是EMF3呼叫PlayEnhMetaFile,而EMF5呼叫EnumEnhMetaFile。PlayEnhMetaFile函数有下面的语法:

PlayEnhMetaFile (hdc, hemf, &rect) ;  

第一个参数是要显示的MetaFile的设备内容句柄。第二个参数是增强型MetaFile句柄。第三个参数是指向描述设备内容平面上矩形的RECT结构的指针。MetaFile图像大小被缩放过,以便刚好能够显示在不超过该矩形的区域内。

EnumEnhMetaFile有5个参数,其中3个与PlayEnhMetaFile一样(虽然RECT结构的指针已经移到参数表的末尾)。

EnumEnhMetaFile的第三个参数是列举函数的名称,它用于呼叫EnhMetaFileProc。第四个参数是希望传递给列举函数的任意数据的指针,这里将该参数简单地设定为NULL。

现在看一看列举函数。当呼叫EnumEnhMetaFile时,对于MetaFile中的每一个记录,GDI都将呼叫EnhMetaFileProc一次,包括表头纪录和文件结束记录。通常列举函数传回TRUE,但它可能传回FALSE以略过剩下的列举程序。

该列举函数有5个参数,稍后会描述它们。在这个程序中,我仅把前4个参数传递给PlayEnhMetaFileRecord,它使GDI执行由该记录代表的函数呼叫,好像您明确地呼叫它一样。

EMF5使用EnumEnhMetaFile和PlayEnhMetaFileRecord得到的结果与EMF3呼叫PlayEnhMetaFile得到的结果一样。区别在于EMF5现在直接介入了MetaFile的显示程序,并能够存取各个MetaFile记录。这是很有用的。

列举函数的第一个参数是设备内容句柄。GDI从EnumEnhMetaFile的第一个参数中简单地取得此句柄。列举函数把该句柄传递给PlayEnhMetaFileRecord来标识图像显示的目的设备内容。

我们先跳到列举函数的第三个参数,它是指向ENHMETARECORD型态结构的指针,前面已经提到过。这个结构描述实际的MetaFile记录,就像它亲自在MetaFile中编码一样。

您可以写一些程序代码来检查这些记录。您也许不想把某些记录传送到PlayEnhMetaFileRecord函数。例如,在EMF5.C中,把下行插入到PlayEnhMetaFileRecord呼叫的前面:

if (pEmfRecord->iType != EMR_LINETO)  

重新编译程序,执行它,将只看到矩形,而没有两条线。或使用下面的叙述:

if (pEmfRecord->iType != EMR_SELECTOBJECT)  

这个小改变会让GDI用内定对象显示图像,而不是用MetaFile所建立的画笔和画刷。

程序中不应该修改MetaFile记录,不过先不要担心这一点。先来看一看程序18-7EMF6。

程序18-7 EMF6
EMF6.C  
/*----------------------------------------------------------------------------  
  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 lpszCmdLine, 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   = 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.EMFMetaFile,因此要在VisualC++中执行这个程序之前先执行过EMF3程序。

EMF6展示了如果在显示MetaFile之前要修改它们,解决方法是非常简单的:做个被修改过的副本出来就好了。您可以看到,列举程序一开始使用malloc配置一块MetaFile记录大小的内存,它是由传递给该函数的pEmfRecord结构的nSize字段表示的。这个内存块的指针储存在变量pEmfr中,pEmfr本身是指向ENHMETARECORD结构的指针。

程序使用CopyMemory把pEmfRecord指向的结构内容复制到pEmfr指向的结构中。现在我们就可以做些修改了。程序检查记录是否为EMR_RECTANGLE型态,如果是,则用EMR_ELLIPSE取代iType字段。PEmfr指标被传递到PlayEnhMetaFileRecord然后被释放。结果是程序画出一个椭圆而不是矩形。其它的内容的修改方式都是相同的。

当然,我们的小改变很容易起作用,因为Rectangle和Ellipse函数有同样的参数,这些参数都定义同一件事-图画的边界框。要进行范围更广的修改需要一些不同MetaFile记录格式的相关知识。

另一个可能性是插入一、两个额外的记录。例如,用下面的叙述代替EMF6.C中的if叙述:

if (pEmfr->iType == EMR_RECTANGLE)  
{   PlayEnhMetaFileRecord (hdc, pHandleTable, pEmfr, nObjects) ;   pEmfr->iType = EMR_ELLIPSE ;  
}  

无论何时出现Rectangle记录,程序都会处理此记录并把它更改为Ellipse,然后再显示。现在程序将画出矩形和椭圆。

现在讨论一下在列举MetaFile时GDI对象处理的方式。

在MetaFile表头中,ENHMETAHEADER结构的nHandles字段是比在MetaFile中建立的GDI对象数还要大的值。因此,对于EMF5和EMF6中的MetaFile,此字段是3,表示画笔、画刷和其它东西。「其它东西」的具体内容,稍后我会说明。

您会注意到EMF5和EMF6中列举函数的倒数第二个参数,也称作nHandles,它是同一个数,3。

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

typedef struct tagHANDLETABLE  
{   HGDIOBJ objectHandle [1] ;  
}  
HANDLETABLE ;  

HGDIOBJ数据型态是GDI对象的句柄,被定义为32位的指针,类似于所有其它GDI对象。这是那些带有一个元素的数组字段的结构之一。这意味着此字段具有可变的长度。objectHandle数组中的元素数等于nHandles,在此程序中是3。

在列举函数中,可以使用以下表达式取得这些GDI对象句柄:

pHandleTable->objectHandle[i]  

对于3个句柄,i是0、1和2。

每次呼叫列举函数时,数组的第一个元素都将包含所列举的MetaFile句柄。这就是前面提到的「其它东西」。

在第一次呼叫列举函数时,表的第二、第三个元素将是0。它们是画笔和画刷句柄的保留位置。

以下是列举函数运作的方式:MetaFile中的第一个对象建立函数具有EMR_CREATEBRUSHINDIRECT的记录型态,此记录指出了对象编号1。当把该记录传递给PlayEnhMetaFileRecord时,GDI建立画刷并取得它的句柄。此句柄储存在objectHandle数组的元素1(第二个元素)中。当把第一个EMR_SELECTOBJECT记录传递给PlayEnhMetaFileRecord时,GDI发现此对象编号为1,并能够从表中找到该对象实际的句柄,而把它用来呼叫SelectObject。当MetaFile最后删除画刷时,GDI将objectHandle数组的元素1设定回0。

通过存取objectHandle数组,可以使用例如GetObjectType和GetObject等呼叫取得在MetaFile中使用的对象信息。

嵌入图像

列举MetaFile的最重要应用也许是在现有的MetaFile中嵌入其它图像(甚至是整个MetaFile)。事实上,现有的MetaFile保持不变;真正进行的是建立包含现有MetaFile和新嵌入图像的新MetaFile。基本的技巧是把MetaFile设备内容句柄传递给EnumEnhMetaFile,作为它的第一个参数。这使您能够在MetaFile设备内容上显示MetaFile记录和GDI函数呼叫。

在MetaFile命令序列的开头或结尾嵌入新图像是极简单的-就在EMR_HEADER记录之后或在EMF_EOF记录之前。然而,如果您熟悉现有的MetaFile结构,就可以把新的绘图命令嵌入所需的任何地方。如程序18-8EMF7所示。

程序18-8 EMF7
EMF7.C  
/*---------------------------------------------------------------------------  
  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 lpszCmdLine, 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   = 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)  
{   HBRUSHhBrush ;   HPEN hPen ;   LOGBRUSH  lb ;  
  if (pEmfRecord->iType != EMR_HEADER && pEmfRecord->iType != EMR_EOF)PlayEnhMetaFileRecord (hdc, pHandleTable, pEmfRecord, iHandles) ;   if (pEmfRecord->iType == EMR_RECTANGLE)   {  hBrush = SelectObject (hdc, GetStockObject (NULL_BRUSH)) ;  lb.lbStyle = BS_SOLID ;  lb.lbColor = RGB (0, 255, 0) ;  lb.lbHatch = 0 ;  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程序建立MetaFile。

EMF7中的WM_PAINT处理使用PlayEnhMetaFile而不是EnumEnhMetaFile,而且WM_CREATE处理有很大的差别。

首先,程序通过呼叫GetEnhMetaFile取得EMF3.EMF文件的MetaFile句柄,还呼叫GetEnhMetaFileHeader得到增强型MetaFile表头记录,目的是在后面的EnumEnhMetaFile呼叫中使用rclBounds字段。

接下来,程序建立新的MetaFile文件,名为EMF7.EMF。CreateEnhMetaFile函数为MetaFile传回设备内容句柄。然后,使用EMF7.EMF的MetaFile设备内容句柄和EMF3.EMF的MetaFile句柄呼叫EnumEnhMetaFile。

现在来看一看EnhMetaFileProc。如果被列举的记录不是表头纪录或文件结束记录,函数就呼叫PlayEnhMetaFileRecord把记录转换为新的MetaFile设备内容(并不一定排除表头纪录或文件结束记录,但它们会使MetaFile变大)。

如果刚转换的记录是Rectangle呼叫,则函数建立画笔用绿色的轮廓线和透明的内部来绘制椭圆。注意程序中经由储存先前的画笔和画刷句柄来恢复设备内容状态的方法。在此期间,所有这些函数都被插入到MetaFile中(记住,也可以使用PlayEnhMetaFile在现有的MetaFile中插入整个MetaFile)。

回到WM_CREATE处理,程序呼叫CloseEnhMetaFile取得新MetaFile的句柄。然后,它删除两个MetaFile句柄,将EMF3.EMF和EMF7.EMF文件留在磁盘上。

从程序显示输出中可以很明显地看到,椭圆是在矩形之后两条交叉线之前绘制的。

增强型MetaFile阅览器和打印机

使用剪贴簿转换增强型MetaFile非常简单,剪贴簿型态是CF_ENHMetaFile。GetClipboardData函数传回增强型MetaFile句柄,SetClipboardData也使用该MetaFile句柄。复制MetaFile时可以使用CopyEnhMetaFile函数。如果把增强型MetaFile放在剪贴簿中,Windows会让需要旧格式的那些程序也可以使用它。如果在剪贴簿中放置旧格式的MetaFile,Windows将也会自动视需要把内容转换为增强型MetaFile的格式。

程序18-9EMFVIEW所示为在剪贴簿中传送MetaFile的程序代码,它也允许载入、储存和打印MetaFile。

程序18-9 EMFVIEW
EMFVIEW.C  
/*---------------------------------------------------------------------------  
  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)  
{  HACCELhAccel ;   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 = 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 DOCINFOdi = { 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 ;   ENHMETAHEADERheader ;   HDC hdc, hdcPrn ;   HENHMetaFile hemfCopy ;   HMENU   hMenu ;   HPALETTE hPalette ;   int i, iLength, iEnable ;   PAINTSTRUCT  ps ;   RECTrect ;   PTSTR   pBuffer ;  
 switch (message)   {   case WM_CREATE:  // Initialize OPENFILENAME structure  ofn.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 box  
   ofn.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 memory  
   SetCursor (LoadCursor (NULL, IDC_WAIT)) ; ShowCursor (TRUE) ;  
   hemf = GetEnhMetaFile (szFileName) ;  
   ShowCursor (FALSE) ; SetCursor (LoadCursor (NULL, IDC_ARROW)) ;  
 // Invalidate the client area for later update  
   InvalidateRect (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 box  
   ofn.Flags = OFN_OVERWRITEPROMPT ;  
if (!GetSaveFileName (&ofn)) return 0 ;  // Save the EMF to disk file  
   SetCursor (LoadCursor (NULL, IDC_WAIT)) ; ShowCursor (TRUE) ;  
   hemfCopy = CopyEnhMetaFile (hemf, szFileName) ;  
   ShowCursor (FALSE) ; SetCursor (LoadCursor (NULL, IDC_ARROW)) ; if (hemfCopy) { DeleteEnhMetaFile (hemf) ;hemf = hemfCopy ; } else   MessageBox (   hwnd, TEXT ("Cannot save MetaFile"),  szAppName, MB_ICONEXCLAMATION | MB_OK) ; return 0 ;  
case   IDM_FILE_PRINT:  // Show the Print dialog box and get printer DC  
printdlg.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 page  
   rect.left = 0 ; rect.right= GetDeviceCaps (hdcPrn, HORZRES) ; rect.top  = 0 ;rect.bottom   = GetDeviceCaps (hdcPrn, VERTRES) ;  
   bSuccess = FALSE ;  
  // Play the EMF to the printer  
   SetCursor (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 = 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 present  
   if (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 clipboard  
   hemfCopy = CopyEnhMetaFile (hemf, NULL) ;  
   OpenClipboard (hwnd) ; EmptyClipboard () ; SetClipboardData (CF_ENHMetaFile, hemfCopy) ; CloseClipboard () ;  
   if (LOWORD (wParam) == IDM_EDIT_COPY)return 0 ; // fall through if IDM_EDIT_CUT  case   IDM_EDIT_DELETE: if (hemf){ DeleteEnhMetaFile (hemf) ;hemf = NULL ; InvalidateRect (hwnd, NULL, TRUE) ; } return 0 ;  
case   IDM_EDIT_PASTE: OpenClipboard (hwnd) ; hemfCopy = 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 (摘录)  
//Microsoft Developer Studio generated resource script.  
#include "resource.h"  
#include "afxres.h"  
/////////////////////////////////////////////////////////////////////////////  
// Menu  
EMFVIEW MENU DISCARDABLE  
BEGIN  
   POPUP "&File"  
   BEGIN   MENUITEM "&Open\tCtrl+O",  IDM_FILE_OPEN   MENUITEM "Save &As...",IDM_FILE_SAVE_ASMENUITEM SEPARATORMENUITEM "&Print...\tCtrl+P",IDM_FILE_PRINT   MENUITEM SEPARATORMENUITEM "&Properties",   IDM_FILE_PROPERTIESMENUITEM SEPARATORMENUITEM "E&xit", IDM_APP_EXIT  
END  
   POPUP "&Edit"  
   BEGIN   MENUITEM "Cu&t\tCtrl+X",  IDM_EDIT_CUT   MENUITEM "&Copy\tCtrl+C", IDM_EDIT_COPYMENUITEM "&Paste\tCtrl+V",  IDM_EDIT_PASTE   MENUITEM "&Delete\tDel",   IDM_EDIT_DELETE  
   END  
   POPUP "Help"  
   BEGIN   MENUITEM "&About EmfView...",   IDM_APP_ABOUT  
   END  
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, NOINVERT  
VK_DELETE,IDM_EDIT_DELETE,VIRTKEY, NOINVERT  
"X",IDM_EDIT_CUT,VIRTKEY, CONTROL, NOINVERT  
END  
RESOURCE.H (摘录)  
// Microsoft Developer Studio generated include file.  
// Used by EmfView.rc  
#defineIDM_FILE_OPEN 40001  
#defineIDM_FILE_SAVE_AS  40002  
#defineIDM_FILE_PRINT40003  
#defineIDM_FILE_PROPERTIES   40004  
#defineIDM_APP_EXIT  40005  
#defineIDM_EDIT_CUT  40006  
#defineIDM_EDIT_COPY 40007  
#defineIDM_EDIT_PASTE40008  
#defineIDM_EDIT_DELETE  40009  
#defineIDM_APP_ABOUT 40010  

EMFVIEW也支持完整的调色盘处理,以便支持有调色盘编码信息的MetaFile。(透过呼叫Selectpalette来进行)。该程序在CreatePaletteFromMetaFile函数中处理调色盘,在处理WM_PAINT显示MetaFile以及处理WM_QUERYNEWPALETTE和WM_PALETTECHANGED消息时,呼叫这个函数。

在响应菜单中的「Print」命令时,EMFVIEW显示普通的打印机对话框,然后取得页面中可打印区域的大小。MetaFile被缩放成适当尺寸以填入整个区域。EMFVIEW在窗口中以类似方式显示MetaFile。

「File」菜单中的「Properties」项使EMFVIEW显示包含MetaFile表头信息的消息框。

如果打印本章前面建立的EMF2.EMFMetaFile图像,您将会发现用高分辨率的打印机打印出的线条非常细,几乎看不清楚线条的锯齿。打印向量图像时应该使用较宽的画笔(例如,一点宽)。本章后面所示的直尺图像就是这样做的。

显示精确的MetaFile图像

MetaFile图像的好处在于它能够以任意大小缩放并且仍能保持一定的逼真度。这是因为MetaFile通常由一系列向量图形的基本图形组成,基本图形是指线条、填入的区域以及轮廓字体等等。扩大或缩小图像只是简单地缩放定义这些基本图形的所有坐标点。另一方面,对位图来说,压缩图像会遗漏整行列的图素,因而失去重要的显示信息。

当然,MetaFile的压缩并不是完美无缺的。我们所使用的图形输出设备的图素大小是有限的。当MetaFile图像压缩到一定大小时,组成MetaFile的大量线条会变成模糊的斑点,同时区域填入图案和混色看起来也很奇怪。如果MetaFile中包含嵌入的位图或旧的点阵字体,同样会引起类似的问题。

尽管如此,大多数情况下MetaFile可以任意地缩放。这在把MetaFile放入文书处理或桌上印刷文件内时非常有用。一般来说,在上述的应用程序中选择MetaFile图像时,会出现围绕图像的矩形,您可以用鼠标拖动该矩形,将它缩放为任意大小。图像送到打印机时,它也具有同样对应的大小。

然而,有时任意缩放MetaFile并不是个好主意。例如:假设您有一个储存着存款客户签名样本的银行系统,这些签名以一系列折线的方式储存在MetaFile中。将MetaFile变宽或变高会使签名变形,因此应该保持图像的纵横比一致。

在前面的范例程序中,是以显示区域的大小来确定PlayEnhMetaFile呼叫使用的围绕矩形范围。所以,如果改变程序窗体的大小,也就改变了图像的大小。这与在文书处理文件中改变MetaFile图像大小的概念相似。

正确地显示MetaFile图像(以特定的度量单位或用适当的纵横比),需要使用MetaFile表头中的大小信息并根据此信息设定矩形结构。

在本章剩下的范例程序中将使用名为EMF.C的程序架构,它包括打印处理的程序代码、资源描述档EMF.RC和表头文件RESOURCE.H。程序18-10显示了这些文件以及EMF8.C程序,该程序使用这些文件显示一把6英寸的直尺。

程序18-10 EMF8
EMF8.C  
/*---------------------------------------------------------------------------  
  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 width   SelectObject (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 marks   for (i = 1 ; i < 96 ; i++)   {   if (i % 16 == 0) iHeight = cy /  2 ; // incheselse   if (i %  8 == 0) iHeight = cy /  3 ;// half incheselse   if (i %  4 == 0) iHeight = cy /  5 ; // quarter incheselse   if (i %  2 == 0) iHeight = cy /  8 ; // eighths   else  iHeight = cy / 12 ;// sixteenths  
MoveToEx (hdc, i * cx / 96, cy, NULL) ; LineTo   (hdc, i * cx / 96, cy - 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 numbers  
 for (i = 1 ; i <= 5 ; i++)   {  ch = (TCHAR) (i + '0') ;  TextOut (hdc, i * cx / 6, cy / 2, &ch, 1) ;   }  // Clean up   DeleteObject (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 ("EMF8\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  
/*----------------------------------------------------------------------------  
  EMF.C --  Enhanced MetaFile Demonstration Shell Program (c) Charles Petzold, 1998  
-----------------------------------------------------------------------------*/  
#include <windows.h>  
#include <commdlg.h>  
#include "..\\emf8\\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)  
{   TCHARszResource [] = 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   = 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 DOCINFOdi ;   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)  
{   BOOLbSuccess ;   static intcxClient, 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 Program\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 (摘录)  
//Microsoft Developer Studio generated resource script.  
#include "resource.h"  
#include "afxres.h"  
/////////////////////////////////////////////////////////////////////////////  
// Menu  
EMF MENU DISCARDABLE  
BEGIN  
   POPUP "&File"  
   BEGIN  MENUITEM "&Print...",IDM_PRINTMENUITEM SEPARATOR  MENUITEM "E&xit",  IDM_EXIT  
   END  
   POPUP "&Help"  
   BEGIN  MENUITEM "&About...",  IDM_ABOUT  
   END  
END  
RESOURCE.H (摘录)  
// Microsoft Developer Studio generated include file.  
// Used by Emf.rc  
//  
#define IDM_PRINT40001  
#define IDM_EXIT 40002  
#define IDM_ABOUT40003  

在处理WM_CREATE消息处理期间,EMF.C呼叫名为CreateRoutine的外部函数,该函数建立MetaFile。EMF.C在两个地方呼叫PaintRoutine函数:一处在WM_PAINT消息处理期间,另一处在函数PrintRoutine中以响应菜单命令打印图像。

因为现代的打印机通常比视讯显示器有更高的分辨率,打印MetaFile的能力是测试以特定大小处理图像能力的重要工具。当EMF8建立的MetaFile图像以特定大小显示时,最有意义。该图像是一把6英寸长1英寸宽的直尺,每英寸分为十六格,数字从1到5为TrueType字体。

要绘制一把6英寸的直尺,需要知道一些设备分辨率的知识。EMF8.C中的CreateRoutine函数首先建立MetaFile,然后使用从CreateEnhMetaFile传回的设备内容句柄呼叫GetDeviceCaps四次。这些呼叫取得单位分别为毫米和图素的显示平面的高度与宽度。

这听起来有点怪。MetaFile设备内容通常是作为GDI绘制命令的储存媒介,它不是像视讯显示器或打印机的真正设备,那么它的宽度和高度从何而来?

您可能已经想起来了,CreateEnhMetaFile的第一个参数被称作「参考设备内容」。GDI用这为MetaFile建立设备特征。如果参数设定为NULL(如EMF8中),GDI就把显示器作为参考设备内容。因而,当EMF8使用设备内容呼叫GetDeviceCaps时,它实际上取得有关显示器的信息。

EMF8.C以图素大小除以毫米大小并乘以25.4(1英…嘉?5.4毫米)计算以每英…嫉牡闶ノ坏姆直媛省?/p>

即使我们非常认真地以MetaFile直尺的正确大小绘制它,但是这样子作还是不够的。PlayEnhMetaFile函数在显示图像时,使用作为最后一个参数传递给它的矩形来缩放图像大小,因此该矩形必须设定为直尺的大小。

由于此原因,EMF8中的PaintRoutine函数呼叫GetEnhMetaFileHeader函数来取得MetaFile的表头信息。ENHMETAHEADER结构的rclBounds字段指出以图素为单位的MetaFile图像的围绕矩形。程序使用此信息使直尺位于显示区域中央,如图18-6所示。

图18-6 EMF8得屏幕显示

记住,如果您拿直尺与屏幕中的直尺比较时,两者并不一定非常吻合。如同第五章中所论述的,显示器只能近似地实际显示尺寸。

既然这样做好像有用了,现在就来试着打印图像。哇!如果您有一台300dpi的激光打印机,那么打印出的直尺的宽将会是11/3英寸。这是由于我们依据视讯显示器的图素尺寸来打印。虽然您可能认为这把小尺很可爱,但它不是我们所需要的。让我们再试一试。

ENHMETAHEADER结构包括两个描述图像大小的矩形结构。第一个是rclBounds,EMF8使用这个,它以图素为单位给出图像的大小。第二为rclFrame,它以0.01毫米为单位给出图像的大小。这两个字段之间的关系是由最初建立MetaFile时使用的参考设备内容决定的,在此情况下为显示器(MetaFile表头也包括两个名为szlDevice和szlMillimeters的字段,它们是SIZEL结构,分别以图素单位和毫米单位指出了参考设备的大小,这与从GetDeviceCaps得到的信息一样)。

EMF9使用图像的毫米大小信息,如程序18-11所示。

程序18-11 EMF9
EMF9.C  
/*---------------------------------------------------------------------------  
  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建立的MetaFile,因此确定执行EMF8。

EMF9中的PaintRoutine函数首先使用目的设备内容呼叫GetDeviceCaps四次。像在EMF8中的CreateRoutine函数一样,这些呼叫提供有关设备分辨率的信息。在得到MetaFile句柄之后,它取得表头结构并使用rclFrame字段来计算以0.01毫米为单位的MetaFile图像大小。这是第一步。

然后,函数通过乘以输出设备的图素大小、除以毫米大小再除以100(因为度量尺寸以0.01毫米为单位)将此大小转换为图素大小。现在,PaintRoutine函数具有以图素为单位的直尺大小-与显示器无关。这是适合目的设备的图素大小,而且很容易使图像居中对齐。

就显示器而言,EMF9的显示与EMF8显示的一样。但是如果从EMF9打印直尺,您会看到更正常的直尺-6英…汲ぁ?英…伎怼?/p>

缩放比例和纵横比

您也可能想要使用EMF8建立的直尺MetaFile,而不必显示6英寸的图像。保持图像正确的6比1的纵横比是重要的。如前所述,在文书处理程序或别的应用程序中使用围绕方框来改变MetaFile的大小是很方便的,但是这样会导致某种程度的失真。在这种应用程序中,应该给使用者一个选项来保持原先的纵横比,而不用管围绕方框的大小如何变化。这就是说,传递给PlayEnhMetaFile的矩形结构不能直接由使用者选择的围绕方框定义。传递给该函数的矩形结构只是围绕方框的一部分。

让我们看一看程序18-12 EMF10是如何做的。

程序18-12 EMF10
EMF10.C  
/*--------------------------------------------------------------------------  
  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 ;   floatfScale ;   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 / cxImage, (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英…汲さ闹背咄枷袷实钡耐妓卮笮 ?/p>

然后,程序计算名为fScale的浮点值,它是显示区域宽度与图像宽度的比值以及显示区域高度与图像高度比值两者的最小值。这个因子在计算围绕矩形前用于增加图像的图素大小。

MetaFile中的映像方式

前面绘制的直尺单位有英…迹灿泻撩住U庵止ぷ魇褂肎DI提供的各种映射方式似乎非常适合。但是我坚持使用图素,并「手工」完成所有必要的计算。为什么呢?

答案很简单,就是将映射方式与MetaFile一起使用会十分混乱。我们不妨实验一下。

当使用MetaFile设备内容呼叫SetMapMode时,该函数在MetaFile中像其它GDI函数一样被编码。如程序18-13EMF11显示的那样。

程序18-13 EMF11
EMF11.C  
/*---------------------------------------------------------------------------  
  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 width  
 SelectObject (hdc, CreatePen (PS_SOLID, cx / 72 / 6, 0)) ; // Rectangle surrounding entire pen (with adjustment)   if (GetVersion () & 0x80000000)  ` // Windows 98 Rectangle (hdc, 0, -2, cx + 2, cy) ;   else   
// Windows NT  Rectangle (hdc, 0, -1, cx + 1, cy) ;  
// Tick marks  for (i = 1 ; i < 96 ; i++)   {if(i %16== 0)   iHeight = cy /2  ;  // inches   else if(i % 8 == 0)iHeight = cy /   3 ;// half incheselse if(i % 4 == 0)iHeight = cy /   5 ;// quarter incheselse if(i % 2 == 0)iHeight = cy /   8 ;// eighthselse iHeight = cy /12 ;  // sixteenths  
  MoveToEx (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 numbers  
 for (i = 1 ; i <= 5 ; i++)   {  ch = (TCHAR) (i + '0') ;  TextOut (hdc, i * cx / 6, cy / 2, &ch, 1) ;   }  // Clean up   DeleteObject (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.top  = (cyArea - cyImage) / 2 ;   rect.right= (cxArea + cxImage) / 2 ;   rect.bottom   = (cyArea + cyImage) / 2 ;  
  PlayEnhMetaFile (hdc, hemf, &rect) ;   DeleteEnhMetaFile (hemf) ;  
}  

EMF11中的CreateRoutine函数比EMF8(最初的直尺MetaFile程序)中的那个简单,因为它不需要呼叫GetDeviceCaps来确定以每英…嫉闶ノ坏南允酒鞣直媛省O喾矗珽MF11呼叫SetMapMode将映像方式设定为MM_LOENGLISH,其逻辑单位等于0.01英…肌R蚨背叩拇笮∥?00×100个单位,并将这些数值传递给DrawRuler。

除了MoveToEx和LineTo呼叫绘制直尺的刻度外,EMF11中的DrawRuler函数与EMF9中的一样。当以图素单位绘制时(内定的MM_TEXT映像方式),垂直轴上的单位沿着屏幕向下增长。对于MM_LOENGLISH映像方式(以及其它度量映像方式),则向上增长。这就需要修改程序代码。同时,也需要更改Rectangle函数中的调节因子。

EMF11中的PaintRoutine函数基本上与EMF9中的相同,那个版本的程序能在显示器和打印机上以正确尺寸显示直尺。唯一不同之处在于EMF11使用EMF11.EMF文件,而EMF9使用EMF8建立的EMF8.EMF文件。

EMF11显示的图像基本上与EMF9所显示的相同。因此,在这里可以看到将SetMapMode呼叫嵌入MetaFile能够简化MetaFile的建立,而且不影响以其正确大小显示MetaFile的机制。

映射与显示

在EMF11中计算目的矩形包括对GetDeviceCaps的几个呼叫。我们的第二个目的是使用映像方式代替这些呼叫。GDI将目的矩形的坐标视为逻辑坐标。为这些坐标使用MM_HIMETRIC似乎是个好方案,因为它使用0.01毫米作为逻辑单位,与增强型MetaFile表头中用于围绕矩形的单位相同。

程序18-14中所示的EMF12程序,保留了EMF8中使用的DrawRuler处理方式,但是使用MM_HIMETRIC映像方式显示MetaFile。

程序18-14 EMF12
EMF12.C  
/*---------------------------------------------------------------------------  
  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 width   SelectObject (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 marks   for (i = 1 ; i < 96 ; i++)   {if (i % 16 == 0) iHeight = cy /  2 ;  // incheselse if (i %  8 == 0) iHeight = cy /  3 ;   // half incheselse if (i %  4 == 0) iHeight = cy /  5 ;   // quarter inches   else if (i %  2 == 0) iHeight = cy /  8 ;   // eighthselse iHeight = cy / 12 ; // sixteenths  
MoveToEx (hdc, i * cx / 96, cy, NULL) ;  LineTo   (hdc, i * cx / 96, cy - 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 numbers  
 for (i = 1 ; i <= 5 ; i++)  
{  ch = (TCHAR) (i + '0') ;  TextOut (hdc, i * cx / 6, cy / 2, &ch, 1) ;  
}  // Clean up   DeleteObject (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 ("EMF13\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 ;  POINTpt ;   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.top  = (pt.y + cyImage) / 2 ;   rect.right= (pt.x + cxImage) / 2 ;   rect.bottom   = (pt.y - cyImage) / 2 ;  
  PlayEnhMetaFile (hdc, hemf, &rect) ;   DeleteEnhMetaFile (hemf) ;  
}  

EMF12中的PaintRoutine函数首先将映像方式设定为MM_HIMETRIC。像其它度量映像方式一样,y值沿着屏幕向上增长。然而,原点坐标仍在屏幕的左上角,这就意味显示区域内的y坐标值是负数。为了纠正这个问题,程序呼叫SetViewportOrgEx将原点坐标设定在左下角。

设备坐标(cxArea,0)位于屏幕的右上角。把该坐标点传递给DPtoLP(「设备坐标点到逻辑坐标点」)函数,得到以0.01毫米为单位的显示区域大小。

然后,程序加载MetaFile,取得文件表头,并找到以0.01毫米为单位的MetaFile大小。这样计算目的矩形在显示区域居中对齐的位置就变得十分简单。

现在我们看到了在建立MetaFile时能够使用映射方式,显示它时也能使用映射方式。我们能一起完成它们吗?

如程序18-15 EMF13展示的那样,这是可以的。

程序18-15 EMF13
EMF13.C  
/*---------------------------------------------------------------------------  
  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 ;   POINTpt ;   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.top  = (pt.y + cyImage) / 2 ;   rect.right= (pt.x + cxImage) / 2 ;   rect.bottom   = (pt.y - cyImage) / 2 ;  
  PlayEnhMetaFile (hdc, hemf, &rect) ;   DeleteEnhMetaFile (hemf) ;  
}  

在EMF13中,由于直尺 MetaFile 已由EMF11建立,所以它没有使用映射方式建立 MetaFile。EMF13只是简单地加载 MetaFile,然后像EMF11一样使用映射方式计算目的矩形。

现在,我们可以建立一些规则。在建立 MetaFile 时,GDI使用对映射方式的任意嵌入修改,来计算以图素和毫米为单位的MetaFile图像的大小。图像的大小储存在 MetaFile 表头内。在显示 MetaFile 时,GDI在呼叫 PlayEnhMetaFile 时根据有效的映像方式建立目的矩形的实际位置,而本来的 MetaFile 中并没有任何记录去更改这个位置。