2012/11/27

GDI+プログラミング

この記事はC/C++の言語本を一通り読み、Windowsプログラミングに多少慣れてきた初学者向け兼備忘録として記載しています。
また、この記事は2012/11/26時点の情報によって記述されています。
C++ビルド環境はVS2005を使用しています。

GDI+プログラミングと銘打っていますが、別段GDI+の全般を使うわけでもなく、単純にGDI+を使用して一般的な画像フォーマットの読み込み実装を外部ライブラリに任せてしまおうというのが主旨です。
gdiplus.dllはXP以降であればOSに標準で付属するとのことなので、XP以降対応であれば、十分選択肢に入る外部ライブラリだと思います。





GDIPlusについて調べると「GDIPlusを使った描画は遅い」と描画性能の問題を取り沙汰されているわけですが、一般的に有名な画像フォーマットを読み込むのに汎用的な画像ライブラリはなかなかないわけで「そろそろ自分で画像を使用した何らかのアプリケーションを作って公開することを目標に活動したい」という時に、各画像フォーマットを全てライブラリをリンク・読み込み処理を実装するのは骨ですので、何とか使用したいところです。

結論から言えば、GDIPlusの画像クラスに実装されているGetHBitmap関数を使用することによってHBITMAPを取得できるので、GDIに描画を任せるのではなく自前でBitBltするなりすることで、GDIPlusは画像の読み込みのみ任せるという形をとれるため、windows前提であれば非常に有用な画像ライブラリです。
また、読み込んだ画像の生データの先頭アドレスとピクセルフォーマット(24bppRGB,32bppARGB等)、幅・高さも取得できるので、生データをそのまま横取りすることもできます。
そのため、GDI+は初学者が各種画像を扱うためには扱いやすいライブラリと言えるでしょう。

○手順


まず、GDIPlusの習得法ですがMSDNに全て書いてあります。
もちろん英語ですが。
MSDNのGDI+への直リンク
(リンク切れの場合、GDIPlusでgoogle先生にお問い合わせください)

上記MSDN記事の下記項目を参照することで、おおよその使い方が把握できます。

英語がわからなくてもソースコードと一緒に眺めていれば、大体何が言いたいのかわかるかと思います。

ここでMSDNに投げっぱなしにしては備忘録にならないので、とりあえず導入~画像データ取得まで書きたいと思います。

まず、PlatformSDKをインストールしてください。
VisualStudio等に付属している場合もあるので、PlatformSDKがあることを確認してください。
gdiplus.hとgdiplus.libはこのSDKに付属しているため、開発を行うためには必須です。
どのバージョンを最低限インストールすればいいのかはOS毎に違ったりするため、別サイトを参考にインストールしてください。

次に、gdiplus.hとgdiplus.libの関連付けをソースコードに記述します。
このとき、windows.hも同様にincludeしてください。
また、CLSIDによるエラーが発生するため、objidl.hも同様に含めるといいでしょう。
最後に、gdiplus.libへの関連付けを行います。
これはpragmaで指定してもいいし、プロジェクトプロパティから直接指定しても構いません。
名前空間を毎回書くのが面倒という場合にはGdiplus名前空間を指定してください。
私はstd名前空間でも記述する方が理解しやすいので、そのまま書いています。

[cpp]
#include <windows.h>
#include <objidl.h>
#include <GdiPlus.h>
// using namespace Gdiplus;
#pragma comment (lib,"Gdiplus.lib")
[/cpp]

初期化処理を追加します。
これはCOMの初期化処理のようなもので、
おまじない程度に考えてください。

[cpp]
#include <windows.h>
#include <objidl.h>
#include <GdiPlus.h>
#pragma comment (lib,"Gdiplus.lib")
INT WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, PSTR, INT iCmdShow)
{
ULONG_PTR gdiplusToken;
Gdiplus::GdiplusStartupInput gdiplusStartupInput;

// Initialize GDI+.
Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);

// TODO:ここに処理を実装

// Finalize GDI+
Gdiplus::GdiplusShutdown(gdiplusToken);
}
[/cpp]

以上で、Gdiplusを使用するためのお膳立てが完了しました。
後は画像ファイルを読み込むだけです。
画像ファイルを扱う場合はGdiplus::BitmapクラスもしくはGdiplus::Imageクラスを使用します。
基本的にGdiplus::Bitmapクラスを使用すれば問題ありません。
BitmapクラスとImageクラスの違いはBitmapクラスがImageクラスを親クラスとした派生クラスで、
Imageクラスにラスターイメージに対するプラスアルファの機能を持っています。
下記に簡単な使用例を記述したコードを記載します。
使い方だけ書いてあるだけで、特に意味があるコードではありませんので、
自己責任で自由に改変してください。

※GDI+プログラミング時の注意点
Gdiplusで使用する文字列は全てUNICODE文字列として扱ってください。
つまり、_TマクロやTEXTマクロは仕様せず、全てL""リテラルを使用してください。
これは、Gdiplusが実装している関数群では専ら文字列引数がWCHAR・wchar_tを使用しているためです。
MBCSを使用したプロジェクトでないと都合が悪いというときは、
mbstowcsもしくはMultiByteToWideCharでマルチバイト文字列からワイド文字列に変換してください。

[cpp]
void OnPaint( HDC hdc )
{

// 画像の読み込み2パターン
Gdiplus::Bitmap clImage( L"test.png", FALSE );
Gdiplus::Bitmap* pImage = NULL;
pImage = Gdiplus::Bitmap::FromFile( L"test.jpg", FALSE );

// 画像情報の取得
pImage->GetHeight();
pImage->GetWidth();
pImage->GetPixelFormat(); // gdipluspixelformats.hに記載されている各種ピクセルフォーマットが返されます。

// GDI+による描画
Gdiplus::Graphics graphics( hdc );
graphics.DrawImage(&clImage, 100, 100);

// HBITMAPによるBitBlt描画
Gdiplus::Color bkcolor( 0xff, 0xff, 0xff );
HBITMAP hBmp;
HBITMAP hOld;
HDC hMem;

clImage.GetHBITMAP( bkcolor, &hBmp);
hMem = CreateCompatibleDC( hdc );
hOld = (HBITMAP)SelectObject( hMem, hBmp );
BitBlt( hdc, 0, 0, clImage.GetWidth(), clImage.GetHeight(), hMem, 0, 0, SRCCOPY );
SelectObject( hMem, hOld );

DeleteDC( hMem );

// 生データの先頭アドレスを取得
Gdiplus::BitmapData bitmapData;
Gdiplus::Rect rect(0, 0, pImage->GetWidth(), pImage->GetHeight());
pImage->LockBits(&rect, Gdiplus::ImageLockModeWrite, PixelFormat24bppRGB, &bitmapData);

bitmapData.Scan0; // ビットマップデータの先頭アドレス
bitmapData.Width;
bitmapData.Height;
bitmapData.Stride; // 一行当たりのpixel数。
bitmapData.PixelFormat;

pImage->UnlockBits(&bitmapData);

delete pImage;
}
[/cpp]

○GDI+で使用できる画像フォーマット


GDI+では次のフォーマットが扱えるとなっています。

  • BMP

  • GIF

  • JPEG

  • PNG

  • TIFF

  • WMF

  • EMF

  • ICON

読込だけなら間違いないのですが、書き込みに関してはリストの下から三つは対応していません。

また、現時点では上記リストのみ対応ですが今後GDI+が変更が加わらないとも限りません。
JPEGにしても、JPEG2000のように改良フォーマットが出ていたりしますので、
JPEG拡張子でもGDI+で読み込めない可能性もありますし、
将来的にディスプレイの品質向上により32bppで足りなくなり、
64bitに拡張されたBMPがそのままbmp拡張子として扱われている可能性もあります。
逆にMicrosoftが画像の仕様拡張に応じて、旧OSでも動作が可能なように
改良版gdiplus.dllをリリースする可能性もあるため、
現時点でEncode/Decodeが対応している拡張子だからと
3rdpartyなdllで投げっぱなしで実装するのは危険です。

何が言いたいかというと、ロード時のエラー処理はしっかり書くことと、
GDI+が対応しているEncoder/Decoderは可能な限り確認して使用しましょうということです。

Encoder/Decoderのリスト取得についてはMSDNの下記に記載されています。

GDI+/Using GDI+/Using Image Encoders and Decoders

簡単に要点のみ書くと以下になります。

  • GetImageDecodersSize/GetImageEncodersSizeを使用してコーデックサイズを取得

  • ImageCodecInfo構造体を上記コーデックサイズ分確報

  • GetImageDecoders/GetImageEncodersを使用してコーデックのリストを取得

  • 確保したImageCodeInfoを解放する

取得したコーデックのリストは下記のような形式で格納されています。

  • image/bmp

  • image/jpeg

  • image/gif

  • image/tiff

  • image/png

  • image/x-icon

  • image/x-emf

  • image/x-wmf


○GdiplusShutdownの働き


読み込んだイメージを即時解放せず、アプリケーションの終了時に一斉解放しようと実装した時、
GdiplusShutdown実行後にdeleteによるオブジェクト削除処理を行うとエラーが発生します。
これをGdiplusShutdown実行前とすると、問題なく削除することができます。
つまり、GDIPlusは生成したオブジェクトを全てライブラリ側で管理しており、
GdiplusShutdownで生成されたオブジェクトを解放しているかのようにも見えます。
そこでMSDNのGdiplusShutdownのリファレンスを確認すると以下の様に記述されています。
Remarks
You must call GdiplusStartup before you create any GDI+ objects, and you must delete all of your GDI+ objects (or have them go out of scope) before you call GdiplusShutdown.
意訳:
備考
GDI+オブジェクトを作成する前にGdiplusStartupを呼び出さないといけない。
また、GdiplusShutdownを呼び出す前に(もしくはスコープ外に出る時に)あなたが生成したGDI+オブジェクトをあなたが削除しなければならない。

上記の備考欄に「生成したら自分で削除しろ」と書いてあるので、
内部で勝手に解放してくれるという手前勝手な実装を期待してはいけなさそうですね。
即時解放せずに、バッファリング・キャッシュする場合は必ずGdiplusShutdownの前にインスタンスの削除処理を入れましょう。
ただ、グローバルなクラスオブジェクトにGDI+オブジェクトを格納していた場合はWinMainがreturnしてからデストラクタが呼び出されるので、
WinMainにGDI+の初期化処理と終了処理を記述するのは柔軟な設計ではないと思います。
GDI+のグローバルなマネージャクラスを作成して、クラス内のコンストラクタ・デストラクタで
StartupとShutdownを実装する等して管理を一元化することで問題を回避したいところです。
後は、グローバルなオブジェクトを作成して、コンストラクタとデストラクタを呼び出してやって、
初期化・後始末が行われるようになっていれば多少は使い勝手がよくなるんじゃないでしょうか。


○後書き


GDIに画像を読み込ませるテクニックのみ記載する形になりましたが、
後はMSDNのリファレンスを参照しながら弄れば自助努力で問題が解決できる環境を整えられたと思います。

多少前提となる知識やVC++の扱い方が省略されていますので、
分かりづらい点があるかもしれません。

0 件のコメント:

コメントを投稿