18. Metafile

Post date: 2012/3/23 上午 05:41:45

18. Metafile

Metafile和向量圖形的關係,就像點陣圖和位元映射圖形的關係一樣。點陣圖通常來自實際的圖像,而metafile則大多是通過電腦程式人為建立的。Metafile由一系列與圖形函式呼叫相同的二進位記錄組成,這些記錄一般用於繪製直線、曲線、填入的區域和文字等。

「畫圖(paint)」程式建立點陣圖,而「繪圖(draw)」程式建立metafile。在優秀的繪圖程式中,能輕易地「抓住」某個獨立的圖形物件(例如一條直線)並將它移動到其他位置。這是因為組成圖形的每個成員都是以單獨的記錄儲存的。在畫圖程式中,這是不可能的-您通常都會侷限於刪除或插入點陣圖矩形塊。

由於metafile以圖形繪製命令描述圖像,因此可以對圖像進行縮放而不會失真。點陣圖則不然,如果以二倍大小來顯示點陣圖,您卻無法得到二倍的解析度,而只是在水平和垂直方向上重複點陣圖的位元。

Metafile可以轉換為點陣圖,但是會丟失一些資訊:組成metafile的圖形物件將不再是獨立的,而是被合併進大的圖像。將點陣圖轉換為metafile要艱難得多,一般僅限於非常簡單的圖像,而且它需要大量處理來分析邊界和輪廓。而metafile可以包含繪製點陣圖的命令。

雖然metafile可以作為圖片剪輯儲存在磁片上,但是它們大多用於程式通過剪貼簿共用圖片的情況。由於metafile將圖片描述為圖像函式呼叫的集合,因而它們既比點陣圖佔用更少的空間,又比點陣圖更與裝置無關。

Microsoft Windows支援兩種metafile格式和支援這些格式的兩組函式。我首先討論從Windows 1.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作為「Windows Metafile」的副檔名),則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") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szAppName, TEXT ("Metafile Demonstration"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam) { static HMETAFILE hmf ; static int cxClient, cyClient ; HBRUSH hBrush ; HDC hdc, hdcMeta ; int x, y ; PAINTSTRUCT ps ; switch (message) { case WM_CREATE: hdcMeta = CreateMetaFile (NULL) ; hBrush = CreateSolidBrush (RGB (0, 0, 255)) ; Rectangle (hdcMeta, 0, 0, 100, 100) ; MoveToEx (hdcMeta, 0, 0, NULL) ; LineTo (hdcMeta, 100, 100) ; MoveToEx (hdcMeta, 0, 100, NULL) ; LineTo (hdcMeta, 100, 0) ; SelectObject (hdcMeta, hBrush) ; Ellipse (hdcMeta, 20, 20, 80, 80) ; hmf = CloseMetaFile (hdcMeta) ; DeleteObject (hBrush) ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; SetMapMode (hdc, MM_ANISOTROPIC) ; SetWindowExtEx (hdc, 1000, 1000, NULL) ; SetViewportExtEx (hdc, cxClient, cyClient, NULL) ; for (x = 0 ; x < 10 ; x++) for (y = 0 ; y < 10 ; y++) { SetWindowOrgEx (hdc, -100 * x, -100 * y, NULL) ; PlayMetaFile (hdc, hmf) ; } EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: DeleteMetaFile (hmf) ; PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

這個程式展示了在使用記憶體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內容的資料,那麼您可以使用

來建立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個位元組,定義如下:

hmf = SetMetaFileBitsEx (iSize, pData) ;

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代號:

該程式還需要定義指向METAFILEPICT型態結構的指標,並為此結構配置一塊整體記憶體:

hmf = CloseMetaFile (hdcMeta) ;

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值:

如果映射方式不是MM_ISOTROPIC或MM_ANISOTROPIC,則可以用xExt和yExt的值設定剪貼矩形。由於這兩個值是邏輯單位,必須用LPtoDP將其轉換為用於剪貼矩形的裝置單位的座標。也可以簡單地儲存這些值以掌握圖像的大小。

對於MM_ISOTROPIC或MM_ANISOTROPIC映射方式,xExt和yExt用來設定視埠範圍。下面有一個用來完成此項任務的函式,如果xExt和yExt沒有建議的大小,則該函式假定cxClient和cyClient分別表示metafile顯示區域的圖素高度和寬度。

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

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,並恢復裝置內容:

然後,對記憶體塊解鎖並關閉剪貼簿:

如果程式使用增強型metafile就可以省去這項工作。當某個應用程式將這些格式放入剪貼簿而另一個程式卻要求從剪貼簿中獲得其他格式時,Windows剪貼簿會自動在老式metafile和增強型metafile之間進行格式轉換。

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

GlobalUnlock (hGlobal) ; CloseClipboard () ;

增強型metafile

「增強型metafile」 格式是在32位元Windows版本中發表的。它包含一組新的函式呼叫、一對新的資料結構、新的剪貼簿格式和新的檔案副檔名.EMF。

這種新的metafile格式最重要的改進是加入可通過函式呼叫取得的更豐富的表頭資訊,這種表頭資訊可用來幫助應用程式顯示metafile圖像。

有些增強型metafile函式使您能夠在增強型metafile(EMF)格式和老式metafile格式(也稱作Windows metafile(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") ; 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 #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 ; RECT rect ; switch (message) { case WM_CREATE: hdcEMF = CreateEnhMetaFile (NULL, NULL, NULL, NULL) ; Rectangle (hdcEMF, 100, 100, 200, 200) ; MoveToEx (hdcEMF, 100, 100, NULL) ; LineTo (hdcEMF, 200, 200) ; MoveToEx (hdcEMF, 200, 100, NULL) ; LineTo (hdcEMF, 100, 200) ; hemf = CloseEnhMetaFile (hdcEMF) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; GetClientRect (hwnd, &rect) ; rect.left = rect.right / 4 ; rect.right = 3 * rect.right / 4 ; rect.top = rect.bottom / 4 ; rect.bottom = 3 * rect.bottom / 4 ; PlayEnhMetaFile (hdc, hemf, &rect) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: DeleteEnhMetaFile (hemf) ; PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

在EMF1的視窗訊息處理程式處理WM_CREATE訊息處理期間,程式首先通過呼叫CreateEnhMetaFile來建立增強型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座標指出該圖像是正方形的,但一般情況下我們卻得不到這樣的圖像。而在某些時候,這又正是我們想要得到的圖像。例如,將圖像嵌入一段文書處理格式的文字中時,可能會要求使用者為圖像指定矩形,並且確保整個圖像恰好位於矩形中而不浪費空間。這樣,使用者可通過適當調整矩形的大小來得到正確的縱橫比。

然而有時候,您也許希望保留圖像最初的縱橫比,因為這一點對於表現視覺資訊尤為重要。例如,警察的嫌疑犯草圖既不能比原型胖也不能比原型瘦。或者您希望保留原來圖像的度量尺寸,圖像必須是兩英吋高,否則就不能正常顯示。在這種情況下,保留圖像的原來尺寸就非常重要了。

同時也要注意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") ; 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 #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表頭資料中極其重要的資訊(這是早期的Windows metafile格式的缺陷之一)。如果該參數為NULL,GDI會計算出尺寸。我比較喜歡讓作業系統替我做這些事,所以將該參數設定為NULL。當應用程式對性能要求比較嚴格時,就需要使用該參數以避免讓GDI處理太多東西。

最後的參數是描述該metafile的字串。該字串分為兩部分:第一部分是以NULL字元結尾的應用程式名稱(不一定是程式的檔案名稱),第二部分是描述視覺圖像內容的說明,以兩個NULL字元結尾。例如用C中的符號「\0」作為NULL字元,則該描述字串可以是「LoonyCad V6.4\0Flying Frogs\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檔案的一堆十六進位代碼。

0000 01 00 00 00 88 00 00 00 64 00 00 00 64 00 00 00 ........d...d... 0010 C8 00 00 00 C8 00 00 00 35 0C 00 00 35 0C 00 00 ........5...5... 0020 6A 18 00 00 6A 18 00 00 20 45 4D 46 00 00 01 00 j...j...EMF.... 0030 F4 00 00 00 07 00 00 00 01 00 00 00 12 00 00 00 ................ 0040 64 00 00 00 00 00 00 00 00 04 00 00 00 03 00 00 d............... 0050 40 01 00 00 F0 00 00 00 00 00 00 00 00 00 00 00 @............... 0060 00 00 00 00 45 00 4D 00 46 00 32 00 00 00 45 00 ....E.M.F.2...E. 0070 4D 00 46 00 20 00 44 00 65 00 6D 00 6F 00 20 00 M.F..D.e.m.o.. 0080 23 00 32 00 00 00 00 00 2B 00 00 00 18 00 00 00 #.2.....+....... 0090 63 00 00 00 63 00 00 00 C6 00 00 00 C6 00 00 00 c...c........... 00A0 1B 00 00 00 10 00 00 00 64 00 00 00 64 00 00 00 ........d...d... 00B0 36 00 00 00 10 00 00 00 C8 00 00 00 C8 00 00 00 6............... 00C0 1B 00 00 00 10 00 00 00 C8 00 00 00 64 00 00 00 ............d... 00D0 36 00 00 00 10 00 00 00 64 00 00 00 C8 00 00 00 6.......d....... 00E0 0E 00 00 00 14 00 00 00 00 00 00 00 10 00 00 00 ................ 00F0 14 00 00 00 ....

圖18-3 EMF2.EMF的十六進位代碼

圖18-3所示的metafile是EMF2在Microsoft Windows NT 4下,視訊顯示器的解析度為1024×768時建立的。同一程式在Windows 98下建立的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程式在Windows 98下建立的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 of file」)的記錄結尾。

增強型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格式對早期Windows metafile所做的最為重要的改進。不需要對metafile檔案使用檔案I/O函式來取得這些表頭資訊。如果具有metafile代號,就可以使用GetEnhMetaFileHeader函式:

GetEnhMetaFileHeader (hemf, cbSize, &emh) ;

第一個參數是metafile代號。最後一個參數是指向ENHMETAHEADER結構的指標。第二個參數是該結構的大小。可以使用類似的GetEnh-MetaFileDescription函式取得描述字串。

如上面所定義的,ENHMETAHEADER結構有100位元組長,但在MF2.EMFmetafile中,記錄的大小包括描述字串,所以大小為0x88,即136位元組。而Windows 98metafile的表頭紀錄不包含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文件中,這個輸出設備被稱作「參考設備(reference device)」。它是依據作為第一個參數傳遞給CreateEnhMetaFile呼叫的代號所指出的裝置內容。如果該參數設為NULL,則GDI使用視訊顯示器。當EMF2建立上面所示的metafile時,正巧是在Windows NT上以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字元的「EMF Demo #2」字串組成。總共18個字元,要是以Unicode方式儲存則為36個字元。無論建立metafile的程式執行在Windows NT還是Windows 98下,該字串始終以Unicode方式儲存。

metafile與GDI物件

我們已經知道了GDI繪圖命令儲存在metafile中方式,現在看一下GDI物件的儲存方式。程式18-4 EMF3除了建立用於繪製矩形和直線的非內定畫筆和畫刷以外,與前面介紹的EMF2程式很相似。該程式也對Rectangle座標的問題提出了一點修改。EMF3程式使用GetVersion來確定執行環境是Windows 98還是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所示:

0000 01 00 00 00 88 00 00 00 60 00 00 00 60 00 00 00 ........`...`... 0010 CC 00 00 00 CC 00 00 00 B8 0B 00 00 B8 0B 00 00 ................ 0020 E7 18 00 00 E7 18 00 00 20 45 4D 46 00 00 01 00 ........EMF..... 0030 88 01 00 00 0F 00 00 00 03 00 00 00 12 00 00 00 ................ 0040 64 00 00 00 00 00 00 00 00 04 00 00 00 03 00 00 d............... 0050 40 01 00 00 F0 00 00 00 00 00 00 00 00 00 00 00 @............... 0060 00 00 00 00 45 00 4D 00 46 00 33 00 00 00 45 00 ....E.M.F.3...E. 0070 4D 00 46 00 20 00 44 00 65 00 6D 00 6F 00 20 00 M.F....D.e.m.o.. 0080 23 00 33 00 00 00 00 00 27 00 00 00 18 00 00 00 #.3.....'....... 0090 01 00 00 00 00 00 00 00 00 00 FF 00 00 00 00 00 ................ 00A0 25 00 00 00 0C 00 00 00 01 00 00 00 5F 00 00 00 %..........._... 00B0 34 00 00 00 02 00 00 00 34 00 00 00 00 00 00 00 4.......4....... 00C0 34 00 00 00 00 00 00 00 00 00 01 00 05 00 00 00 4............... 00D0 00 00 00 00 FF 00 00 00 00 00 00 00 00 00 00 00 ................ 00E0 25 00 00 00 0C 00 00 00 02 00 00 00 2B 00 00 00 %...........+... 00F0 18 00 00 00 63 00 00 00 63 00 00 00 C6 00 00 00 ....c...c....... 0100 C6 00 00 00 1B 00 00 00 10 00 00 00 64 00 00 00 ............d... 0110 64 00 00 00 36 00 00 00 10 00 00 00 C8 00 00 00 d...6........... 0120 C8 00 00 00 1B 00 00 00 10 00 00 00 C8 00 00 00 ................ 0130 64 00 00 00 36 00 00 00 10 00 00 00 64 00 00 00 d...6.......d... 0140 C8 00 00 00 25 00 00 00 0C 00 00 00 07 00 00 80 ....%........... 0150 28 00 00 00 0C 00 00 00 02 00 00 00 25 00 00 00 (...........%... 0160 0C 00 00 00 00 00 00 80 28 00 00 00 0C 00 00 00 ........(....... 0170 01 00 00 00 0E 00 00 00 14 00 00 00 00 00 00 00 ................ 0180 10 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使用的非內定物件數多一。(Platform SDK文件解釋這個多出來的一是「此表中保留的零索引」)。該欄位在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-5 EMF4所示。

程式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) { BITMAP bm ; HBITMAP hbm ; HDC hdc, hdcEMF, hdcMem ; HENHMETAFILE hemf ; PAINTSTRUCT ps ; RECT rect ; switch (message) { case WM_CREATE: hdcEMF = CreateEnhMetaFile (NULL, TEXT ("emf4.emf"), NULL, TEXT ("EMF4\0EMF Demo #4\0")) ; hbm = LoadBitmap (NULL, MAKEINTRESOURCE (OBM_CLOSE)) ; GetObject (hbm, sizeof (BITMAP), &bm) ; hdcMem = CreateCompatibleDC (hdcEMF) ; SelectObject (hdcMem, hbm) ; StretchBlt (hdcEMF,100,100,100,100, hdcMem, 0,0,bm.bmWidth, bm.bmHeight, SRCCOPY) ; DeleteDC (hdcMem) ; DeleteObject (hbm) ; hemf = CloseEnhMetaFile (hdcEMF) ; DeleteEnhMetaFile (hemf) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; GetClientRect (hwnd, &rect) ; rect.left = rect.right / 4 ; rect.right = 3 * rect.right / 4 ; rect.top = rect.bottom / 4 ; rect.bottom = 3 * rect.bottom / 4 ; hemf = GetEnhMetaFile (TEXT ("emf4.emf")) ; PlayEnhMetaFile (hdc, hemf, &rect) ; DeleteEnhMetaFile (hemf) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

為了方便,EMF4載入由常數OEM_CLOSE指出的系統點陣圖。在裝置內容中顯示點陣圖的慣用方法是通過使用CreateCompatibleDC建立與目的裝置內容(此例為metafile裝置內容)相容的記憶體裝置內容。然後,通過使用SelectObject將點陣圖選入該記憶體裝置內容並且從該記憶體裝置內容呼叫BitBlt或StretchBlt把點陣圖畫到目的裝置內容。結束後,刪除記憶體裝置內容和點陣圖。

您會注意到EMF4也呼叫GetObject來確定點陣圖的大小。這對SelectObject呼叫是很必要的。

首先,這份程式碼儲存metafile的空間對GDI來說就是個挑戰。在StretchBlt呼叫前根本沒有別的GDI函式去處理metafile的裝置內容。因此,讓我們來看一看EMF4.EMF裡頭是如何做的,圖18-5只顯示了一部分。

0000 01 00 00 00 88 00 00 00 64 00 00 00 64 00 00 00 ........d...d... 0010 C7 00 00 00 C7 00 00 00 35 0C 00 00 35 0C 00 00 ........5...5... 0020 4B 18 00 00 4B 18 00 00 20 45 4D 46 00 00 01 00 K...K....EMF.... 0030 F0 0E 00 00 03 00 00 00 01 00 00 00 12 00 00 00 ................ 0040 64 00 00 00 00 00 00 00 00 04 00 00 00 03 00 00 d............... 0050 40 01 00 00 F0 00 00 00 00 00 00 00 00 00 00 00 @............... 0060 00 00 00 00 45 00 4D 00 46 00 34 00 00 00 45 00 ....E.M.F.4...E. 0070 4D 00 46 00 20 00 44 00 65 00 6D 00 6F 00 20 00 M.F...D.e.m.o... 0080 23 00 34 00 00 00 00 00 4D 00 00 00 54 0E 00 00 #.4.....M...T... 0090 64 00 00 00 64 00 00 00 C7 00 00 00 C7 00 00 00 d...d........... 00A0 64 00 00 00 64 00 00 00 64 00 00 00 64 00 00 00 d...d...d...d... 00B0 20 00 CC 00 00 00 00 00 00 00 00 00 00 00 80 3F ..............? 00C0 00 00 00 00 00 00 00 00 00 00 80 3F 00 00 00 00 ...........?.... 00D0 00 00 00 00 FF FF FF 00 00 00 00 00 6C 00 00 00 ............l... 00E0 28 00 00 00 94 00 00 00 C0 0D 00 00 28 00 00 00 (...........(... 00F0 16 00 00 00 28 00 00 00 28 00 00 00 16 00 00 00 ....(...(....... 0100 01 00 20 00 00 00 00 00 C0 0D 00 00 00 00 00 00 .. ............. 0110 00 00 00 00 00 00 00 00 00 00 00 00 C0 C0 C0 00 ................ 0120 C0 C0 C0 00 C0 C0 C0 00 C0 C0 C0 00 C0 C0 C0 00 ................ . . . . 0ED0 C0 C0 C0 00 C0 C0 C0 00 C0 C0 C0 00 0E 00 00 00 ................ 0EE0 14 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-6 EMF5所示。此程式使用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程式。同時,需要在Visual C++環境中執行兩個程式,以確保路徑的正確。在處理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-7 EMF6。

程式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,因此要在Visual C++中執行這個程式之前先執行過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-8 EMF7所示。

程式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) { HBRUSH hBrush ; HPEN hPen ; LOGBRUSH lb ; if (pEmfRecord->iType != EMR_HEADER && pEmfRecord->iType != EMR_EOF) PlayEnhMetaFileRecord (hdc, pHandleTable, pEmfRecord, iHandles) ; if (pEmfRecord->iType == EMR_RECTANGLE) { hBrush = 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-9 EMFVIEW所示為在剪貼簿中傳送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) { HACCEL hAccel ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = szAppName ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szAppName, TEXT ("Enhanced Metafile Viewer"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; hAccel = LoadAccelerators (hInstance, szAppName) ; while (GetMessage (&msg, NULL, 0, 0)) { if (!TranslateAccelerator (hwnd, hAccel, &msg)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } } return msg.wParam ; } HPALETTE CreatePaletteFromMetaFile (HENHMETAFILE hemf) { HPALETTE hPalette ; int iNum ; LOGPALETTE * plp ; if (!hemf) return NULL ; if (0 == (iNum = GetEnhMetaFilePaletteEntries (hemf, 0, NULL))) return NULL ; plp = malloc (sizeof (LOGPALETTE) + (iNum - 1) * sizeof (PALETTEENTRY)) ; plp->palVersion = 0x0300 ; plp->palNumEntries = iNum ; GetEnhMetaFilePaletteEntries (hemf, iNum, plp->palPalEntry) ; hPalette = CreatePalette (plp) ; free (plp) ; return hPalette ; } LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam) { static DOCINFO di = { sizeof (DOCINFO), TEXT ("EmfView: Printing") } ; static HENHMETAFILE hemf ; static OPENFILENAME ofn ; static PRINTDLG printdlg = { sizeof (PRINTDLG) } ; static TCHAR szFileName [MAX_PATH], szTitleName [MAX_PATH] ; static TCHAR szFilter[] = TEXT ("Enhanced Metafiles (*.EMF)\0*.emf\0") TEXT ("All Files (*.*)\0*.*\0\0") ; BOOL bSuccess ; ENHMETAHEADER header ; HDC hdc, hdcPrn ; HENHMETAFILE hemfCopy ; HMENU hMenu ; HPALETTE hPalette ; int i, iLength, iEnable ; PAINTSTRUCT ps ; RECT rect ; PTSTR pBuffer ; switch (message) { case WM_CREATE: // Initialize OPENFILENAME 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_AS MENUITEM SEPARATOR MENUITEM "&Print...\tCtrl+P",IDM_FILE_PRINT MENUITEM SEPARATOR MENUITEM "&Properties", IDM_FILE_PROPERTIES MENUITEM SEPARATOR MENUITEM "E&xit", IDM_APP_EXIT END POPUP "&Edit" BEGIN MENUITEM "Cu&t\tCtrl+X", IDM_EDIT_CUT MENUITEM "&Copy\tCtrl+C", IDM_EDIT_COPY MENUITEM "&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 #define IDM_FILE_OPEN 40001 #define IDM_FILE_SAVE_AS 40002 #define IDM_FILE_PRINT 40003 #define IDM_FILE_PROPERTIES 40004 #define IDM_APP_EXIT 40005 #define IDM_EDIT_CUT 40006 #define IDM_EDIT_COPY 40007 #define IDM_EDIT_PASTE 40008 #define IDM_EDIT_DELETE 40009 #define IDM_APP_ABOUT 40010

EMFVIEW也支援完整的調色盤處理,以便支援有調色盤編碼資訊的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 ; // inches else if (i % 8 == 0) iHeight = cy / 3 ; // half inches else if (i % 4 == 0) iHeight = cy / 5 ; // quarter inches else 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) { TCHAR szResource [] = TEXT ("EMF") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; hInst = hInstance ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = szResource ; wndclass.lpszClassName = szClass ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szClass, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szClass, szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } BOOL PrintRoutine (HWND hwnd) { static DOCINFO di ; static PRINTDLG printdlg = { sizeof (PRINTDLG) } ; static TCHAR szMessage [32] ; BOOL bSuccess = FALSE ; HDC hdcPrn ; int cxPage, cyPage ; printdlg.Flags = PD_RETURNDC | PD_NOPAGENUMS | PD_NOSELECTION ; if (!PrintDlg (&printdlg)) return TRUE ; if (NULL == (hdcPrn = printdlg.hDC)) return FALSE ; cxPage = GetDeviceCaps (hdcPrn, HORZRES) ; cyPage = GetDeviceCaps (hdcPrn, VERTRES) ; lstrcpy (szMessage, szClass) ; lstrcat (szMessage, TEXT (": Printing")) ; di.cbSize = sizeof (DOCINFO) ; di.lpszDocName = szMessage ; if (StartDoc (hdcPrn, &di) > 0) { if (StartPage (hdcPrn) > 0) { PaintRoutine (hwnd, hdcPrn, cxPage, cyPage) ; if (EndPage (hdcPrn) > 0) { EndDoc (hdcPrn) ; bSuccess = TRUE ; } } } DeleteDC (hdcPrn) ; return bSuccess ; } LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam) { BOOL bSuccess ; static int cxClient, cyClient ; HDC hdc ; PAINTSTRUCT ps ; switch (message) { case WM_CREATE: CreateRoutine (hwnd) ; return 0 ; case WM_COMMAND: switch (wParam) { case IDM_PRINT: SetCursor (LoadCursor (NULL, IDC_WAIT)) ; ShowCursor (TRUE) ; bSuccess = PrintRoutine (hwnd) ; ShowCursor (FALSE) ; SetCursor (LoadCursor (NULL, IDC_ARROW)) ; if (!bSuccess) MessageBox (hwnd, TEXT ("Error encountered during printing"), szClass, MB_ICONASTERISK | MB_OK) ; return 0 ; case IDM_EXIT: SendMessage (hwnd, WM_CLOSE, 0, 0) ; return 0 ; case IDM_ABOUT: MessageBox (hwnd, TEXT ("Enhanced Metafile Demo 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_PRINT MENUITEM 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_PRINT 40001 #define IDM_EXIT 40002 #define IDM_ABOUT 40003

在處理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英吋為25.4毫米)計算以每英吋的點數為單位的解析度。

即使我們非常認真地以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英吋長、1英吋寬。

縮放比例和縱橫比

您也可能想要使用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 ; float fScale ; HENHMETAFILE hemf ; int cxMms, cyMms, cxPix, cyPix, cxImage, cyImage ; RECT rect ; cxMms = GetDeviceCaps (hdc, HORZSIZE) ; cyMms = GetDeviceCaps (hdc, VERTSIZE) ; cxPix = GetDeviceCaps (hdc, HORZRES) ; cyPix = GetDeviceCaps (hdc, VERTRES) ; hemf = GetEnhMetaFile (TEXT ("..\\emf8\\emf8.emf")) ; GetEnhMetaFileHeader (hemf, sizeof (emh), &emh) ; cxImage = emh.rclFrame.right - emh.rclFrame.left ; cyImage = emh.rclFrame.bottom - emh.rclFrame.top ; cxImage = cxImage * cxPix / cxMms / 100 ; cyImage = cyImage * cyPix / cyMms / 100 ; fScale = min ((float) cxArea / 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英吋長的直尺圖像適當的圖素大小。

然後,程式計算名為fScale的浮點值,它是顯示區域寬度與圖像寬度的比值以及顯示區域高度與圖像高度比值兩者的最小值。這個因數在計算圍繞矩形前用於增加圖像的圖素大小。

metafile中的映射方式

前面繪製的直尺單位有英吋,也有毫米。這種工作使用GDI提供的各種映射方式似乎非常適合。但是我堅持使用圖素,並「手工」完成所有必要的計算。為什麼呢?

答案很簡單,就是將映射方式與metafile一起使用會十分混亂。我們不妨實驗一下。

當使用metafile裝置內容呼叫SetMapMode時,該函式在metafile中像其他GDI函式一樣被編碼。如程式18-13 EMF11顯示的那樣。

程式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 inches else if(i % 4 == 0)iHeight = cy / 5 ; // quarter inches else if(i % 2 == 0)iHeight = cy / 8 ; // eighths else 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來確定以每英吋點數為單位的顯示器解析度。相反,EMF11呼叫SetMapMode將映射方式設定為MM_LOENGLISH,其邏輯單位等於0.01英吋。因而,直尺的大小為600×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 ; // inches else if (i % 8 == 0) iHeight = cy / 3 ; // half inches else if (i % 4 == 0) iHeight = cy / 5 ; // quarter inches else 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 ("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 ; POINT pt ; int cxImage, cyImage ; RECT rect ; SetMapMode (hdc, MM_HIMETRIC) ; SetViewportOrgEx (hdc, 0, cyArea, NULL) ; pt.x = cxArea ; pt.y = 0 ; DPtoLP (hdc, &pt, 1) ; hemf = GetEnhMetaFile (TEXT ("emf12.emf")) ; GetEnhMetaFileHeader (hemf, sizeof (emh), &emh) ; cxImage = emh.rclFrame.right - emh.rclFrame.left ; cyImage = emh.rclFrame.bottom - emh.rclFrame.top ; rect.left = (pt.x - cxImage) / 2 ; rect.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 ; POINT pt ; int cxImage, cyImage ; RECT rect ; SetMapMode (hdc, MM_HIMETRIC) ; SetViewportOrgEx (hdc, 0, cyArea, NULL) ; pt.x = cxArea ; pt.y = 0 ; DPtoLP (hdc, &pt, 1) ; hemf = GetEnhMetaFile (TEXT ("..\\emf11\\emf11.emf")) ; GetEnhMetaFileHeader (hemf, sizeof (emh), &emh) ; cxImage = emh.rclFrame.right - emh.rclFrame.left ; cyImage = emh.rclFrame.bottom - emh.rclFrame.top ; rect.left = (pt.x - cxImage) / 2 ; rect.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中並沒有任何記錄去更改這個位置。