2.8 TMetafileTMeatfileはメタファイルを扱うためのグラフィッククラスです。また TMetafileCanvas クラスを使うとメタファイルを新規に作成出来ます。TMetafile はメタファイルを扱う上で若干のの問題を抱えています。本節では、VCL のメタファイルの扱いかたを境界ボックスの取り扱いを中心に解説します。 尚、本節では Delphi 1 と Delphi 5以降(Delphi 6 も含む)の TMetafile を説明します。 Delphi 2、3、4 の TMetafile は問題が多く、使用に耐えないので、Delphi 5 以降の TMeatfile を使うことを強くお勧めします。 2.8.1 メタファイルの基礎知識ここでは、解説を始める前にメタファイルの復習を簡単にしておきます。尚、メタファイルの基本を説明をするつもりはありませんので、詳しく知りたい方は、MSDN のドキュメントを参照してください。 2.8.1.1 旧メタファイルとスケーラブルなメタファイルWindows 3.X ではメタファイル(以後旧メタファイル)は基本的には GDI ファンクションの記録です。しかし、好き勝手にメタファイルを作ってしまうと、描画(Play)した時にどの程度の大きさで表示されるかが皆目判らず困ってしまいます。このため、スケーラブルなメタファイルと APM ヘッダというものが考え出されました スケーラブルなメタファイルは、記録されている GDI ファンクションが SetMapMode MM_ANISOTROPIC, SetWindowOrg, Set WindowExt の3個から始まります。SetMapMode MM_ANISOTROPIC は省略されている場合がありますが、その場合はマッピングモードとして MM_ANISOTROPIC が仮定されます。 メタファイルの作成者は、メタファイルに SetWindowOrg/SetWindowExt API を埋め込んで Window(境界ボックス)を指定し、その中に図形を描くようにします。メタファイルを表示するアプリケーションは、描画(Play)する前に、SetMapMode API でマッピングモードを MM_ANISOTROPIC に切り替え、Viewport の位置を SetViewportOrg, SetViewportExt API で指定します。こうすることで、メタファイル内の絵を、デバイス座標上の望みの位置に、望みの大きさで描画することが出来ます(下図参照)。 図2.8-1 スケーラブルメタファイル 2.8.1.2 APM ヘッダWindows 3.X 形式のメタファイルのファイル形式は、Windows の API (CreateMetafile/CloseMetafile)が作るメタファイルのファイル形式ではなく、Aldus Placeable Format と呼ばれる形式で流通しています。これは API が作るメタファイルの先頭に APM (Aldus Placeable Format)ヘッダ付けたもので、拡張子は一般に wmf です。APM ヘッダを以下に示します。 TMetafileHeader = packed record Key: Longint; Handle: SmallInt; Box: TSmallRect; Inch: Word; Reserved: Longint; CheckSum: Word; end; key は $9AC6CDD7 の固定値で、CheckSum はヘッダの先頭 10 WORD の XOR をとったものです。Handle は 0 固定で Reserved は使われていません。Box は前で説明した論理座標上での境界ボックスで、メタファイルに埋め込まれた SetWindowOrg/SetWindowExt で指定する Window(境界ボックス) と同じものです。Box がヘッダにあるおかげで、境界ボックスの大きさと形を簡単に知ることができます。 Inch は論理座標を実寸法に換算するための係数で、単位は Metafile Unit/インチ です。Metafile Unit は Box で使われている論理座標の単位です。この係数と Box(境界ボックス)から、推奨されるメタファイルの実寸法を求められます。本来スケーラブルなメタファイルに決まった寸法というのは有りませんが、APM ヘッダはデフォルトの寸法を与えます。例えば、アプリケーションが最初にメタファイルを画面に貼り付ける時のデフォルトの大きさを決める時や、CAD の図面等、実寸法を重要視する図形をプリンタに正確な大きさで描く時などに使用されます。 Inch は 500〜1000 ぐらいの値を取り、たいていは 1000 程度です。Inch=1000 の旧メタファイルは、1000 dpi の仮想的なデバイス上で描画された図形を記録したものと考えればわかり易いと思います。境界ボックスの大きさはだいたい、500〜2000くらいですから、実寸法は 0.5〜2インチぐらいの大きさになります。Inch は Windows 3.X の 座標が 16Bit しかないことから、円の一部を書く時などのことを考慮し、オーバーフローを起こさないように、1440 以下に設定することが推奨されています。 2.8.1.3 エンハンストメタファイルMicrosoft は Win32 で、新しいメタファイルを導入しました。新機能の詳しい説明はMicrosoft のドキュメントを読んでください。重要な点は
の5点です。以下に、エンハンストメタファイルのヘッダレコードを以下に示し、この5点を説明します。 TEnhMetaHeader = packed record iType: DWORD; nSize: DWORD; rclBounds: TRect; // 全ての図形を囲む最小矩形 単位 ピクセル rclFrame: TRect; // 境界ボックス 単位 0.01mm dSignature: DWORD; nVersion: DWORD; nBytes: DWORD; nRecords: DWORD; nHandles: Word; sReserved: Word; nDescription: DWORD; nDescription: DWORD; offDescription: DWORD; nPalEntries: DWORD; // パレットの色数 szlDevice: TSize; // 参照デバイスの幅と高さ 単位ピクセル szlMillimeters: TSize; // 参照デバイスの幅と高さ 単位 mm end; まず、1.の点ですが、旧メタファイルではメタファイルを作成するためのデバイスコンテキストの取り扱いが、スクリーンやプリンタなどと大きく異なりました(特に SelectObject API の取り扱いが)。このため Delphi 1 ではメタファイルを 作るための TCanvas から継承したクラスを作成することが困難で、メタファイルの作成はサポートされませんでした。しかし、Delphi 2 以降、TMetafile は Enhanced Metafile を扱うクラスになったため、メタファイルを TMetafileCanvas で簡単に作成できるようになりました。 次に、2と3と4を説明します。 エンハンストメタファイルでは、メタファイルが作成された時のデバイス情報を持つようになりました。このデバイスを参照デバイスと呼びます。参照デバイスの情報は、例えば、ビットマップの描画など、図形のピクセル単位の大きさが描画の品質に大きな影響を与える場合、参照デバイス上で図形が何ピクセルで描画されていたかを知るのに便利です。上記のEnhanced Metafile のヘッダレコード内の szlDevice, szlMillimeters が参照デバイスの情報で、それぞれ参照デバイスの描画面のピクセル単位の大きさと、 mm 単位の大きさです。これは旧メタファイルの Inch に相当する情報を提供します。 旧メタファイルでは、境界ボックスの大きさは、2.8.1.2 で説明したように、厳密には標準ではない APM ヘッダから取得するか、メタファイルを解析して、SetWindowOrg と SetWindowExt のパラメータから拾い出すしかありませんでしたが、エンハンストメタファイルでは境界ボックスを正式にサポートし、メタファイルのヘッダ内に境界ボックスが含まれるようになりました。 境界ボックス(ヘッダレコード内のrclFrame) は論理座標ではなく、0.01 mm 単位の座標系で表されます。この座標系は MM_HIMETRIC と良く似ていますが、原点がデバイスの描画面の左上です。エンハンストメタファイルの境界ボックスは実寸法で表されていることに注意してください。 エンハンストメタファイルではメタファイルを作る時に CreateEnhMetafile API に明示的に境界ボックスを指定できます。但し、rclFrame にメタファイル内の図形が収まるようにするのはメタファイルの作成者の仕事です。 ヘッダ内の rclBounds は参照デバイスのデバイス座標(ピクセル座標)で表現された、メタファイル内の全ての図形を囲む最小の矩形です。必ずしも、境界ボックス(rclFrame)と一致しないことに注意してください。rclBounds はメタファイルが作られる時自動的に Windows が抽出します。rclBounds は指定された枠内に絵がぴったり収まるように描く時に便利です。 エンハンストメタファイルは最初からスケーラブルで、旧メタファイルの様に意識的に SetMapMode/SetWindowOrg/SetWindowExt を埋め込む必要は有りません。メタファイルの作成者は CreateEnhMetafile API で境界ボックスを指定してメタファイルに描画すれば自動的にスケーラブルなメタファイルが出来上がります。表示する時も、マッピングモードの切り替えや Viewport の設定は不要です。PlayEnhMetafile API で、表示先の矩形を論理座標 で指定するだけで望みの位置に望みの大きさでスケーリングされたメタファイルを描画できます。 ヘッダの内容からも判りますように、エンハンストメタファイルにはパレットの情報も含まれるようになりました。パレットを選択して実体化する GDI ファンクションをメタファイルに記録すると、パレットのエントリが全てメタファイルに記録されます。これは後で GetEnhMetaFilePaletteEntries API を使って、メタファイルから取り出すことができます。 最後に、5の点ですが、 Enhanced Metafile を作成する際、メタファイルにリージョンを書き込んでおくと、メタファイル描画される際、PlayEnhMetafile API でメタファイルが変形されて描画される場合でも、リージョンが正しく変形されて適用されます。 Enhanced Metafile が文字を含む場合、リージョンと同様、文字もできうる限り正確に変形されて描画されます。つまり、個々文字の位置はメタファイルの変形にあわせて、正確な座標(位置)に描画されます。また個々の文字の高さや幅もできうる限り正確に変形されます。但し、ビットマップフォントのような、文字の高さ、幅に厳しい制限のあるものは正しい大きさにはなりません。その場合でも「位置」だけは正確に描画されます。 尚、TMetafile はメタファイルのファイルを読むときに、このエンハンストメタファイルのフォーマットと Aldus Placeable Metafaile Format の両方を自動的に判別し、旧メタファイルの場合はエンハンストメタファイルに変換して取り込みます。 2.8.2 TMetafile の Width/Height プロパティ, MMWidth/MMHeight プロパティ の意味TMetafile の Width/Height/MMWidth/MMHeight プロパティ は境界ボックスの大きさを表し、メタファイル描画時に大きさが特に与えられなかった場合のデフォルトの大きさを決める プロパティ です。Width/Height は Pixel 単位で、 MMWidth/MMHeight は 0.01 mm 単位での境界ボックスの大きさを表しています。この正確な意味はこれから説明しますが、実は、Delphi 1 の古い TMetafile との間の互換性を保つため少々煩雑なことになってしまっています。 そこで、まず、最も簡単な、Delphi 1.0J の TMetafile が旧メタファイルのファイル(*.wmf)を読むケースから説明し、それから、Delphi 5/6の TMetafile がエンハンストメタファイルのファイル(*.emf)を読むケースを、さらに問題が発生する Delphi 5/6 の TMetafile で旧メタファイルのファイルを読むケースを説明します。 2.8.2.1 Delphi 1.0J の TMetafile で 旧メタファイルのファイルを読む場合(参考)Delphi 1.0J の TMetafile には MMWidth/MMHeight プロパティ が無く、Width/Height プロパティ だけが境界ボックスを表します。境界ボックスの計算は以下の通りです。
要するに、TMetafile の Width/Height は APM ヘッダから求めた境界ボックスの実寸法の大きさを、スクリーンデバイスのピクセル数に換算したものなのです。 2.8.2.2 TMetafile(Delphi 5 以降) がエンハンストメタファイルのファイルを読む場合この場合も非常に単純です。TMetafile はエンハンストメタファイルのヘッダ内の rclFrame の大きさをそのまま MMWidth/MMHeight プロパティ に代入します。つまり、MMWidth/MMHeight は境界ボックスの幅と高さを 0.01mm で表したものです。 一方 Width/Height は以下のように計算されます Width := MMWidth * (参照デバイスの描画面の X 方向の大きさ in Pixels) / (参照デバイスの描画面の X 方向の大きさ in mm) / 100; つまり、Width/Height はメタファイルが作られた時の参照デバイス上での境界ボックスのおおきさを参照デバイスの Pixel 単位で表したものです。スクリーンデバイスではなく、参照デバイスである点に注意してください。 2.8.2.3 TMetafile(Delphi 5/6) が旧メタファイルのファイルを読む場合TMetafile が旧メタファイルを読むと、まず、APM ヘッダ内の Box と Inch から 0.01mm 単位の境界ボックスの大きさを下記のように計算し、MMWidth/MMHeight プロパティ に設定します。 MMWidth := (Box.Right - Box.Left) / inch * 2540; Delphi 5/6の TMetafile には 1.0J の TMetafile と同様、Inch プロパティ が有り、エンハンストメタファイルのファイルを読んだ場合は 0 に、旧メタファイルのファイルを読んだ場合は APM ヘッダの Inch がそのまま設定されます。Width/Height プロパティ が参照された時の Width/Height を求める計算は Inch プロパティ を見て切り替わります。inch = 0 の時の計算は 2.8.2.2 の通りですが、旧メタファイルを読んだ場合(つまり、Inch <> 0)の計算は以下のようになります。 Width := MMWidth * (スクリーンデバイスの Pixels per Inch) / 2540;
上記の式に MMWidth/MMHeight の計算式を代入すると、Width/Height プロパティ の値は Delphi 1.0J と全く同じになり、そういう意味では互換性が保たれています。 2.8.1.3 で Delphi 5/6 のTMetafile は旧メタファイルをエンハンストメタファイルに変換して保持することを述べました。このエンハンストメタファイルの境界ボックスと上記の Width/Height/MMWidth/MMHeight プロパティ の関係がどうなっているかを考察してみます。 TMetafile は旧メタファイルを読むと SetWinMetafileBits API で旧メタファイルをエンハンストメタファイルに変換します。実際には、メタファイルの境界ボックスと図形との間の隙間を正しく保つため、2回に分けて変換しています。これは Delphi 5 で追加された処理なので Delphi 4 以前では問題が起きます。これが Delphi 4 以前の TMetafile を推奨しない理由です。 変換で旧メタファイルはエンハンストメタファイルに変換されますが、SetWinMetafileBits API は APM ヘッダを見ないので、得られた境界ボックス rclFrame は APMヘッダから得られる値とは一致しません。一般的に rclFrame の大きさは画面のサイズの近辺の値になります(そうならない場合もあります)。 典型的な旧メタファイルの例として、APM ヘッダ内の境界ボックスが (1000, 1000, 2000, 2000)、Inch が 1000 で、(1000, 1000, 2000, 2000) の楕円が描かれている旧メタファイルを使い、スクリーンデバイスは 1024 X 768 96 dpi のものを使用すると仮定します。 APM ヘッダから、境界ボックスの大きさは 1インチX1インチですから、TMetafile に読み込むと Width/Height プロパティ は共に 96(96DPIの画面の場合) になります。MMWidth/MMHeight プロパティ は共に 1インチ X 2540 = 2540 in 0.01 mm(25.4 mm) になります。一方、rclFrame はこれとは無関係に、大きさが 32000, 24000近辺の値になります(そうならないことも有ります)。 このように rclFrame と MMWidth/MMHeight が異なるように設定されるのは、Borland の技術者が、なんとかメタファイルを正常に表示しようとしている努力の結果です。SetWinFileBits は元の図形の大きさの情報を捨ててしまうので、APM ヘッダの情報を何とか生かそうとしているわけです。 2.8.3.4 TMetafile(Delphi 5/6) のSaveToStream(SaveToFile)メソッドの問題点TMetafile は、メタファイルをファイルにセーブする時 拡張子が wmf のファイル名が指定されるか、Enhanced プロパティ が False の場合、エンハンストメタファイルを旧メタファイルに変換して書き込みます。変換には GetWinMetafileBits API が使われますが、この API は論理座標上の 1 unit がエンハンストメタファイルの 参照デバイス上の1ピクセルに対応しているものとして旧メタファイルを作成します。 Inch プロパティが 0 以外にセットされている場合、例えば旧メタファイルを TMetafile に読み込んだ場合は Inch が0以外にセットされますが、これを旧メタファイルとしてファイルにセーブすると、TMetafile はAPM ヘッダの境界ボックス Box の値を (0, 0, MMWidth * Inch div 2540 , MMHeight * Inch div 2540) に設定し、APMヘッダの Inch は Inch プロパティ のものを設定します。これはAPMヘッダと定義どおりの変換であり、なんの問題もありません。メタファイルは正しく旧メタファイルとしてセーブされます。 次にエンハンストメタファイルを読み込んで、それを旧メタファイル形式で書き出す場合はどうでしょうか? この場合は Inch プロパティが0なので APMヘッダの Inch は 96 に設定されます、Box の値は (0, 0, MMWidth * 96 div 2540 , MMHeight * 96 div 2540) になります。少々 Inch の精度を落としすぎているような気がしますが、MMWidth/MMHeight は 0.01mm 単位なので、最悪数十万オーダーの値になる一方、旧メタファイルは座標が16bitなので境界ボックスの大きさが32768 以下でなくてはならないので、Inchiを小さめに設定する必要があります。まあ妥当な値と言えるでしょう。大きな問題はありません。メタファイルは正しく旧メタファイルとしてセーブされます(もちろん旧メタファイルがサポートしていない図形やパレットの情報は失われます)。 最後に旧メタファイルを TMetafile に読んで Enhanced メタファイルとしてファイルに書く場合について考察します。この場合だけ問題が起きます。 前述したように、旧メタファイルを TMetafile に読み込んだ場合、MMWidth/MMHeight の値と Enhanced Metafile のヘッダレコードのrclFrame の値とは異なっています。TMetafile はメタファイルを Enhanced メタファイルとして書く場合、TMeatfile内のメタファイルを全く変更せずにそのまま書きますから、MMWidth/MMHeight の値は失われてしまいます。 結果として、こうして作ったファイルをもう一度 TMetafile に読み込むと MMWidth/MMHeight の値が全く異なったものに変化してしまいます。注意が必要です。メタファイルの境界矩形の寸法が重要な場合は、このような使い方は避けるべきでしょう。 2.8.4 TMetafile の描画Delphi 1.0J ではメタファイルの描画は、マッピングモードを変更して、PlayMetafile を呼んで描画しています。つまり、マッピングモードを MM_ANISOTROPIC に設定し、 Window Extent を (0, 0, Width, Height) に、Viewport を指定された描画先に設定することで、スケーリングを行い描画します。一方 Delphi 5/6 TMetafile は PlayEnhMetafile で描画先の矩形を指定するだけです。スケーリングは PlayEnhMetafile API が行います。 Delphi 5/6 TMetafile はエンハンストメタファイルがパレットをサポートするので、パレットを 選択/実体化してから描画します。従って、画面が256色モードの場合でも正しい色で描画することができます。 2.8.5 TMetafileCanvasTMetafileCanvas は新たにメタファイルを作成するための Canvas です。使い方はそれほど難しくはありませんが、ここでは何故、TBitmap の様に TMetafile に Canvas プロパティ が無いのかという点と、幾つかの注意点を説明します。 2.8.5.1 なぜ、TMetafileCanvas は TMetafile の一部ではないのか?TBitmap と比べて、TMetafile は Canvas の使い方が全く異なります。TBitmap には Canvas プロパティ が有り、いつでも TBitmap に図形を書き込むことが出来ます。一方、TMetafile は TMetafileCanvas を作成してから図形を書き込まなければなりません。 こうなっているのは、メタファイルには始めと終わりが有るからです。メタファイルを作るには、CreateEnhMetafile API で空のメタファイルを作成し、図形を書き込んでから CloseEnhMetafile API でクローズしなければなりません。もし、TMetafile に Canvas プロパティ が有ると、メタファイルをクローズする契機がはっきりしないので困ります。 以上の理由から、TMetafileCanvas は TMetafile とは別のオブジェクトとして作成します。TMetafileCanvas はコンストラクタ Create でメタファイルの出力先の TMetafile を指定します。TMetafileCanvas は デストラクタ Destroy で破棄される時、TMetafile の中に、メタファイルを残します。 2.8.5.2 境界ボックスはいつ決まるか新たにメタファイルを作るには、境界ボックスを指定しなければなりません。この辺が、TMetafileCanvas を使う上でのややこしいところです。 境界ボックスを、指定するには、TMetafile の Width/Height/MMWidth/MMHeight プロパティを使います。詳しい説明を入る前に、空の TMetafile に関して少し説明しておかなければなりません。 前に説明しましたように、Width, Height, MMWidth, MMHeight はメタファイルの境界ボックスを表わします。しかし、TMetafile が Create で作成された直後は内部にメタファイルが無いため、これらの値は全て0になっています。この状態の TMetafile の Width/Height/MMWidth/MMHeight に値を設定したらどうなるのでしょうか? TMetafile のなかにメタファイルがない場合、Inch やデバイス情報が無いので、ピクセルの大きさが何mm なのかが判りません。従って、Width と MMWidth, Height と MMHeight の間の係数が判りません。そこで、空の TMetafile は、各 プロパティ を独立に管理します。つまり、Width と MMWidth Peoprty の値が独立に設定できるのです。これが後でメタファイルを TMetafileCanvas で作成される時に境界ボックスを決めるのに使われます。もちろん、既にメタファイルが入っている場合は、前に説明したように、Width と MMWidth, Height と MMHeight との間には一定の関係が有ります。 TMetafileCanvas が Create されると、TMetafileCanvas は MMWidth, MMHeight の値で境界ボックスをメタファイルに設定します。TMetafileCanvas は MMWidth と MMHeight を境界ボックスに設定する前に、以下のアルゴリズムで MMWidth/MMHeight を調整します。 幅の決定
高さの決定
要するに、MMWidth/MMHeight が最優先。Width/Height が設定されていて、MMWidth/MMHeight が 0ならば、Width/Height から計算、そうでなければ、参照デバイスの描画面の大きさが使われるということです。例えば、最もシンプルな var mf: TMetafile; mc: TMetafileCanvas; . . mf := TMetafile.Create; mc := TMetafileCanvas.Create(mf, 0); とした場合、境界ボックスはスクリーン全体になります。 2.8.5.3 Canvas への描画TMetafileCanvas の取り扱い方は、他の Canvas と同じです。座標の単位は参照デバイスのピクセルです。2つ注意点があります。 ひとつめは、境界ボックスを意識した大きさで描画しないといけないことぐらいです。例えば 1024 x 768 ピクセルのスクリーンを持つシステムで var mf: TMetafile; mc: TMetafileCanvas; . . mf := TMetafile.Create; mc := TMetafile.Canvas(mf, 0) mc := mc.Ellipse(0, 0, 100, 100); mc.Free; Image1.Picture.Graphic := mf; mf.Free; とした時、イメージコントロールの Stretch プロパティ が True だと、非常に小さい歪んだ円が表示されます。これは、メタファイルの境界ボックスが 1024 x 768 ピクセルで、イメージコントロールが境界ボックスをイメージコントロールの枠に合うようにメタファイルを変形して描画するためです。 もうひとつは、文字を描くときです。TMetafileCanvas は参照デバイスを指定して作成しますが、このとき TMetafileCanvas の解像度(DPI値)はデバイスと同じになります。しかし、残念ながら、TMetafileCanvas のFont.PixelsPerInch プロパティはスクリーンのものに設定されてしまいます。 TMetafileCanvas のHandle プロパティ(Device Contextハンドル)を使って、GetDeviceCapas(XXXXCancas.Handle, LOGPIXELSY) で正しい DPI値を取得し、Font.PixelPerInch にセットしてから文字の描画を行ってください。TMetafileCanvas が自動的にやってくれてもよさそうなものですが、やってくれません。おそらく実装漏れでしょう。 2.8.6 ファイル/ストリームとの入出力TMetafile の LoadFromStream/LoadFromFile/SaveToStream/SaveToFile は メモリまたはファイルと入出力を行ないます。 Delphi 1.0J では APM 形式のみをサポートします。Delphi 5/6 の TMetafile は APM 形式とエンハンストメタファイル形式の両方をサポートします。読み込みでは、ファイルの形式を自動判別して読み込みますが、旧メタファイルの場合はエンハンストメタファイルに変換します。 書き込みでは、Enhanced プロパティ に従って、書き込む形式が変わります。Enhanced が True の場合は、メタファイルはそのままファイルに書き込まれます。Enhanced プロパティ が False の場合は、旧メタファイル形式で書き込まれます。但し、 SaveToFile メソッドで書き込む場合で、ファイルの拡張子が wmf の場合、必ず旧メタファイル形式で書き込まれます。これは VCL の提供するサービスようなものですが、知らないと驚きます。注意しましょう。 2.8.7 クリップボードとの入出力TMetafile の LoadFromClipBoardFormat と SaveToClipBoardFormatは TPicture の同名のメソッドの下請けルーチンで、クリップボードと TMetafile のインスタンスとの間でメタファイルをやり取りするのに使われるヘルパルーチンです。
2.8.8 まとめ
|