The Old Metafile Format

Metafiles either can exist temporarily in memory or can be saved as disk files. To an application, these two processes are quite similar; in particular, all the file I/O that would otherwise be involved in saving and loading data to and from disk-based metafiles is handled by Windows.

Simple Use of Memory Metafiles

You create a metafile in the old format by first creating a metafile device context with a call to CreateMetaFile. You can then use most of the GDI drawing functions to draw on this metafile device context. These GDI calls don't really draw on any real device, however. Instead, they are stored within the metafile. When you close the metafile device context, you get back a handle to the metafile. You can then "play" this metafile on a real device context, which is equivalent to executing the GDI functions in the metafile.

CreateMetaFile takes a single argument. This can be either NULL or a filename. If NULL, the metafile is stored in memory. If it's a filename—the extension .WMF, for "Windows Metafile," is customary—then the metafile is stored in a disk file.

The program METAFILE shown in Figure 18-1 shows how to create a memory metafile during the WM_CREATE message and display the image 100 times during the WM_PAINT message.

Figure 18-1. The METAFILE program.


   METAFILE.C -- Metafile Demonstration Program
                 (c) Charles Petzold, 1998
#include <windows.h>


int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdLine, int iCmdShow)
     static TCHAR szAppName [] = TEXT ("Metafile") ;
     HWND         hwnd ;
     MSG          msg ;
     WNDCLASS     wndclass ;         = 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"),
                 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)) ;

angle (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) ;

This program demonstrates the use of the four metafile functions essential in using a memory metafile. The first is CreateMetaFile, which the program calls with a NULL argument during processing of the WM_CREATE message. The function returns a handle to a metafile device context. METAFILE then draws two lines and one blue ellipse using this metafile DC. These function calls are stored in a binary form in the metafile. The CloseMetaFile function returns a handle to the metafile. Notice that the metafile handle is stored in a static variable because it will be used later.

The metafile contains a binary representation of the GDI function calls, which are two MoveToEx calls, two LineTo calls, a SelectObject call (indicating the blue brush), and an Ellipse call. No mapping mode or transform is implied by the coordinates. They are simply stored as numbers in the metafile.

During the WM_PAINT message, METAFILE sets up a mapping mode and calls PlayMetaFile to draw the object 100 times in the window using PlayMetaFile. The coordinates of the function calls in the metafile are interpreted in the context of the current transform set up for the destination device context. In calling PlayMetaFile, in effect you're repeating all the calls that you made between CreateMetaFile and CloseMetaFile when you originally created the metafile during the WM_CREATE message.

As with any GDI object, metafile objects should be deleted before a program terminates. This occurs during the WM_DESTROY message with the DeleteMetaFile function.

The results of the METAFILE program are shown in Figure 18-2.

Click to view at full size.

Figure 18-2. The METAFILE display.

Storing Metafiles on Disk

In the above example, the NULL argument to CreateMetaFile meant that we wanted to create a metafile stored in memory. We can also create a metafile stored on a disk as a file. This method is preferred for large metafiles because it uses less memory space. On the down side, a metafile stored on disk requires a disk access every time you play it.

To convert METAFILE to using a disk-based metafile, you need to replace the NULL argument to CreateMetaFile with a filename. At the conclusion of the WM_CREATE processing, you can call DeleteMetaFile with the metafile handle. The handle has been deleted but the disk file remains behind.

During processing of the WM_PAINT message, you can get a metafile handle to this disk file by calling GetMetaFile:

hmf = GetMetaFile (szFileName) ;

Now you can play this metafile just as before. When processing of the WM_PAINT message is over, you can delete the metafile handle:

DeleteMetaFile (hmf) ;

When it comes time to process the WM_DESTROY message, you don't have to delete the metafile, because it was deleted at the end of the WM_CREATE message and at the end of each WM_PAINT message. But you should still delete the disk file like so,

DeleteFile (szFileName) ;

unless, of course, you want to keep the file around.

You can make a metafile a programmer-defined resource as discussed in Chapter 10. You'd simply load it as a data block. If you have a block of data with the contents of a metafile, you can create a metafile using

hmf = SetMetaFileBitsEx (iSize, pData) ;

SetMetaFileBitsEx has a companion function, GetMetaFileBitsEx, that copies the contents of a metafile to a block of memory.

Old Metafiles and the Clipboard

The old metafiles have a nasty flaw. If you have a handle to an old-style metafile, how can you determine how large the image will be when you play it? Unless you start digging into the internals of the metafile itself, you can't.

Moreover, when a program obtains an old-style metafile from the clipboard, it has the most flexibility in working with it if the metafile has been designed to be played in an MM_ISOTROPIC or MM_ANISOTROPIC mapping mode. The program that receives the metafile can then scale the image by simply setting viewport extents before playing the metafile. But if the mapping mode is set to MM_ISOTROPIC or MM_ANISOTROPIC within the metafile, the program that receives the metafile is stuck. The program can make GDI calls only before or after the metafile is played. It can't make a GDI call in the middle of a metafile.

To solve these problems, old-style metafile handles are not directly put into the clipboard and retrieved by other programs. Instead, the metafile handle is part of a "metafile picture," which is a structure of type METAFILEPICT. This structure allows the program that obtains the metafile picture from the clipboard to set the mapping mode and viewport extents itself before playing the metafile.

The METAFILEPICT structure is 16 bytes long and defined like so:

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

For all the mapping modes except MM_ISOTROPIC and MM_ANISOTROPIC, the xExt and yExt values are the size of the image in units of the mapping mode given by mm. With this information, the program that copies the metafile picture structure from the clipboard can determine how much display space the metafile will encompass when it is played. The program that creates the metafile can set these values to the largest x-coordinates and y-coordinates it uses in the GDI drawing functions that enter the metafile.

For the MM_ISOTROPIC and MM_ANISOTROPIC mapping modes, the xExt and yExt fields function differently. You will recall from Chapter 5 that a program uses the MM_ISOTROPIC or MM_ANISOTROPIC mapping mode when it wants to use arbitrary logical units in GDI functions independent of the measurable size of the image. A program uses MM_ISOTROPIC when it wants to maintain an aspect ratio regardless of the size of the viewing surface and MM_ANISOTROPIC when it doesn't care about the aspect ratio. You will also recall from Chapter 5 that after a program sets the mapping mode to MM_ISOTROPIC or MM_ANISOTROPIC, it generally makes calls to SetWindowExtEx and SetViewportExtEx. The SetWindowExtEx call uses logical units to specify the units the program wants to use when drawing. The SetViewportExtEx call uses device units based on the size of the viewing surface (for instance, the size of the window's client area).

If a program creates an MM_ISOTROPIC or MM_ANISOTROPIC metafile for the clipboard, the metafile should not itself contain a call to SetViewportExtEx because the device units in that call would be based on the display surface of the program creating the metafile and not on the display surface of the program that reads the metafile from the clipboard and plays it. Instead, the xExt and yExt values should assist the program that obtains the metafile from the clipboard in setting appropriate viewport extents for playing the metafile. But the metafile itself contains a call to set the window extent when the mapping mode is MM_ISOTROPIC or MM_ANISOTROPIC. The coordinates of the GDI drawing functions within the metafile are based on these window extents.

The program that creates the metafile and metafile picture follows these rules:

Here's some sample code for a program creating a metafile and copying it to the clipboard. If the metafile uses the MM_ISOTROPIC or MM_ANISOTROPIC mapping mode, the first calls in the metafile should be to set the window extent. (The window extent is fixed in the other mapping modes.) Regardless of the mapping mode, the window origin can also be set:

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

The coordinates in the drawing functions of the metafile are based on these window extents and the window origin. After the program uses GDI calls to draw on the metafile device context, the metafile is closed to get a handle to the metafile:

hmf = CloseMetaFile (hdcMeta) ;

The program also needs to define a pointer to a structure of type METAFILEPICT and allocate a block of global memory for this structure:

[other program lines]
hGlobal= GlobalAlloc (GHND | GMEM_SHARE, sizeof (METAFILEPICT)) ;
pMFP = (LPMETAFILEPICT) GlobalLock (hGlobal) ;

Next, the program sets the four fields of this structure:

pMFP->mm   = MM_...  ;
pMFP->xExt = ...  ;
pMFP->yExt = ...  ;
pMFP->hMF  = hmf  ;

GlobalUnlock (hGlobal) ;

The program then transfers the global memory block containing the metafile picture structure to the clipboard:

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

Following these calls, the hGlobal handle (the memory block containing the metafile picture structure) and the hmf handle (the metafile itself) become invalid for the program that created them.

Now for the hard part. When a program obtains a metafile from the clipboard and plays this metafile, the following steps must take place:

  1. The program uses the mm field of the metafile picture structure to set the mapping mode.

  2. For mapping modes other than MM_ISOTROPIC or MM_ANISOTROPIC, the program uses the xExt and yExt values to set a clipping rectangle or simply to determine the size of the image. For the MM_ISOTROPIC and MM_ANISOTROPIC mapping modes, the program uses xExt and yExt to set the viewport extents.

  3. The program then plays the metafile.

Here's the code. You first open the clipboard, get the handle to the metafile picture structure, and lock it:

OpenClipboard (hwnd) ;
hGlobal = GetClipboardData (CF_METAFILEPICT) ;
pMFP = (LPMETAFILEPICT) GlobalLock (hGlobal) ;

You can then save the attributes of your current device context and set the mapping mode to the mm value of the structure:

SaveDC (hdc) ;
SetMappingMode (pMFP->mm) ;

If the mapping mode isn't MM_ISOTROPIC or MM_ANISOTROPIC, you can set a clipping rectangle to the values of xExt and yExt. Because these values are in logical units, you have to use LPtoDP to convert the coordinates to device units for the clipping rectangle. Or you can simply save the values so that you know how large the image is.

For the MM_ISOTROPIC or MM_ANISOTROPIC mapping mode, you use xExt and yExt to set the viewport extent. One possible function to perform this task is shown below. This function assumes that cxClient and cyClient represent the pixel height and width of the area in which you want the metafile to appear if no suggested size is implied by xExt and yExt.

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) ;

This code assumes that both xExt and yExt are 0, greater than 0, or less than 0 (which should be the case). If the extents are 0, no size or aspect ratio is suggested. The viewport extents are set to the area in which you want to display the metafile. Positive values of xExt and yExt are a suggested image size in units of 0.01 mm. The GetDeviceCaps function assists in determining the number of pixels per 0.01 mm, and this value is multiplied by the extent values in the metafile picture structure. Negative values of xExt and yExt indicate a suggested aspect ratio but not a suggested size. The value iScale is first calculated based on the aspect ratio of the size in millimeters corresponding to cxClient and cyClient. This scaling factor is then used to set a viewport extent in pixels.

With this job out of the way, you can set a viewport origin if you want, play the metafile, and return the device context to normal:

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

Then you unlock the memory block and close the clipboard:

GlobalUnlock (hGlobal) ;
CloseClipboard () ;

If your program uses enhanced metafiles, you don't have to do this work. The Windows clipboard will convert between the old metafile format and the enhanced metafile format when one application puts one of these formats into the clipboard and another application requests the other format from the clipboard.