- 追加された行はこの色です。
- 削除された行はこの色です。
皆さんお待ちかねの(?)
GDIを使った描画を扱います
GDIとはGraphics Device Interfaceの略で
グラフィカルオブジェクトの表示と、ディスプレイやプリンターのような出力デバイスへの転送のためのWindowsの規格です
今回は
・ペンを使った直線、曲線の描画
・ブラシを使った円、矩形の描画
・ビットマップ画像の描画
・フォント指定した文字列の描画
の基本的な描画を行います
&ref(paint.GIF);
先にことわっておきます
かなりめんどくさいです
まず、Win32APIの描画の概念図から
&ref(hdc.jpg);
真ん中のHDCというのは、デバイスコンテキストのハンドル(ポインタ)です
デバイスコンテキストとは、出力対象を指します
大抵の場合、出力対象とはアプリケーションのクライアント領域(描画領域)を指します
HDCに結ばれているものを描画オブジェクトと呼びます(正式な呼び方ではないかもしれませんが…)
・HPENはペンのハンドルです。
ペンは線を書くのに使います。
・HBITMAPはビットマップのハンドルです。
ビットマップ画像(.bmp)を指します。
ここで、ビットマップ以外は取り扱えないの?って話になるのですが
GDI+を使えばそれ以外のフォーマットの画像でも簡単に読み込み描画することができます(それについてはまた今度)
・HBRUSHはブラシのハンドルです。
ブラシは図形の塗りつぶしに使います。
・HFONTはフォントのハンドルです。
フォントは文字を書くときに文字サイズや書式を決めます。
・HRGNはリージョンのハンドルです。
リージョンは、ウィンドウの形の決めることができます
今回は使いません、機会があったら取り上げようと思います。
・HPALETTEはパレットのハンドルです。
ここでは取り扱いません
SIZE(30){ COLOR(#ffaa00){デバイスコンテキストには関連付けられた描画オブジェクトが必ず1種類ずつ存在します}}
つまりデバイスコンテキストは
常にペン、ビットマップ、ブラシ、フォント、リージョン、パレットのハンドルを一種類ずつ持っています
(イメージ的には、千手観音みたいなもの)
ここで注意してほしいのは、
SIZE(30){ COLOR(#ffaa00){デバイスコンテキストは同時に同じ種類の描画オブジェクトを2つ以上持てません}}
たとえば、ペンを2つ同時に持つことはできません
なので
太い線を書いた後、細い線を書きたいなと思ったら
太いペンで太い線を書いた後、細いペンに持ちかえなければなりません
ブラシやフォントに関しても同じです
-------------------------------------------------------------------
今回のソースコードです。
//------------------------------------------------------//
// ウィンドウ描画メッセージ処理
//
// 引数: hWnd ウィンドウハンドル
//------------------------------------------------------//
void OnPaint(HWND hWnd){
// 描画開始
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd,&ps);
// ペン作成
HPEN hPen = CreatePen(PS_SOLID,5,RGB(255,0,0));
HPEN hNullPen = CreatePen(PS_NULL,1,RGB(0,0,0));
HPEN hDefPen = (HPEN)SelectObject(hdc,hPen);
// ブラシ作成
HBRUSH hBrush = CreateSolidBrush(RGB(0,255,0));
HBRUSH hDefBrush = (HBRUSH)SelectObject(hdc,hBrush);
// ビットマップ読み込み
HINSTANCE hInstance = (HINSTANCE)GetModuleHandle(NULL); // インスタンスハンドル取得
HBITMAP hBmp = (HBITMAP)LoadImage(hInstance,_T("test.bmp"),IMAGE_BITMAP,0,0,LR_DEFAULTCOLOR | LR_SHARED | LR_LOADFROMFILE);
BITMAP Bmp;
GetObject(hBmp,sizeof(BITMAP),&Bmp);
HDC hBmpDC = CreateCompatibleDC(hdc);
HBITMAP hDefBmp = (HBITMAP)SelectObject(hBmpDC, hBmp);
// フォント作成
LOGFONT logfont = {};
logfont.lfHeight = 30; // 文字高さ
logfont.lfWidth = 12; // 文字幅
logfont.lfEscapement = 30; // 文字送りの方向とX軸の角度
logfont.lfOrientation = logfont.lfEscapement; // ベースラインとX軸との角度
logfont.lfWeight = FW_BOLD; // フォントの太さ
logfont.lfItalic = TRUE; // イタリック体指定
logfont.lfUnderline = TRUE; // 下線付き指定
logfont.lfStrikeOut = TRUE; // 打ち消し線指定
logfont.lfCharSet = SHIFTJIS_CHARSET; // キャラクタセット
logfont.lfOutPrecision = OUT_DEFAULT_PRECIS; // 出力精度
logfont.lfClipPrecision = CLIP_DEFAULT_PRECIS; // クリッピングの精度
logfont.lfQuality = DEFAULT_QUALITY; // 出力品質
logfont.lfPitchAndFamily= FF_DONTCARE|DEFAULT_PITCH;// ファミリー
_tcscpy(logfont.lfFaceName,_T("MS ゴシック")); // フォント名
HFONT hFont = CreateFontIndirect(&logfont);
HFONT hDefFont = (HFONT)SelectObject(hdc,hFont);
// 直線描画
MoveToEx(hdc,10,100,NULL); // 始点をセット
LineTo(hdc,10,200); // 始点から割り当てられたペンで終点まで直線描画
// 曲線描画
POINT pt[4] = {};
pt[0].x = 50;pt[0].y = 50;
pt[1].x = 100;pt[1].y = 150;
pt[2].x = 50;pt[2].y = 250;
pt[3].x = 150;pt[3].y = 250;
// 点情報から割り当てられたペンでベジェ曲線描画、点の数は3の倍数+1でなければならない
PolyBezier(hdc,pt,4);
// 楕円描画
SelectObject(hdc,hNullPen); // 枠がいらない
RECT PointRect;
PointRect.left = -5;
PointRect.top = -5;
PointRect.right = 5;
PointRect.bottom = 5;
for(int i = 0;i < 4;++i)
Ellipse(hdc,pt[i].x + PointRect.left,pt[i].y + PointRect.top,pt[i].x + PointRect.right,pt[i].y + PointRect.bottom);
// 矩形描画
SelectObject(hdc,hNullPen); // 枠がいらない
RECT rect = {30,130,50,150};
Rectangle(hdc,rect.left,rect.top,rect.right,rect.bottom); // ブラシで指定された矩形を塗りつぶす
// 画像表示
BitBlt(hdc,200,0,Bmp.bmWidth,Bmp.bmHeight,hBmpDC,0,0,SRCCOPY);
int X = 0;
int Y = 0;
// 文字列描画
const TCHAR* szText = _T("Hello World");
SetTextColor(hdc,RGB(0,0,255)); // 文字色を青に変更
TextOut(hdc,X,Y,szText,(int)_tcslen(szText)); // 文字列描画
// 使い終わったら元の描画オブジェクトに戻す
SelectObject(hdc,hDefPen);
SelectObject(hdc,hDefBrush);
SelectObject(hBmpDC, hDefBmp);
SelectObject(hdc, hDefFont);
// 使わなくなった描画オブジェクトは破棄
DeleteObject(hPen);
DeleteObject(hNullPen);
DeleteObject(hBrush);
DeleteObject(hBmp);
DeleteObject(hDefFont);
// 使わなくなったデバイスコンテキストを破棄
DeleteDC(hBmpDC);
// 描画終了
EndPaint(hWnd,&ps);
}
//------------------------------------------------------//
// ウィンドウ破棄メッセージ処理
//
// 引数: hWnd ウィンドウハンドル
//------------------------------------------------------//
void OnDestroy(HWND hWnd){
PostQuitMessage(0);
}
//-----------------------------------------------------//
// ウィンドウプロシージャ
// デフォルトの処理はDefWindowProc関数で行う
//
// 引数: hWnd ウィンドウハンドル
// msg メッセージ
// wParam パラメータ
// lParam パラメータ
//
// 戻り値:処理結果
//-----------------------------------------------------//
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam){
switch(msg){
HANDLE_MSG(hWnd,WM_PAINT,OnPaint);
HANDLE_MSG(hWnd,WM_DESTROY,OnDestroy);
}
return DefWindowProc(hWnd, msg, wParam, lParam);
}
描画を行うタイミングというものがあります(好きな時に描画していいわけではありません)
それは、WM_PAINTというメッセージが送られてきたときです
このWM_PAINTは、ウィンドウが作成されたときやウィンドウのサイズが変わったりしたときに
ウィンドウプロシージャに送られてくるメッセージです
メッセージクラッカーで自作のOnPaint関数に処理させます
HANDLE_MSG(hWnd,WM_PAINT,OnPaint);
描画するときは
必ずBeginPaint関数とEndPaint関数の間で描画しなければなりません
// 描画開始
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd,&ps);
// ここに描画処理
// 描画終了
EndPaint(hWnd,&ps);
------------------------------------------------------
ペンを作成するには、
CreatePen関数を使います
// ペン作成
HPEN hPen = CreatePen(PS_SOLID,5,RGB(255,0,0));
HPEN hNullPen = CreatePen(PS_NULL,1,RGB(0,0,0));
CreatePen関数の
第一引数はペンのスタイルです
PS_SOLID 普通の線
PS_DASH 破線
PS_DOT 点線
PS_NULL 空のペン(描画しない)
などがあります
第2引数はペンの太さです
第3引数はペンの色です
引数がCOLORREFとなっていますが
RGBマクロを使うとよいでしょう
各種色成分は0〜255の範囲で指定します
たとえば、RGB(255,0,0)は赤になります
もっと詳しく知りたい人はmsdn公式を見てください
http://msdn.microsoft.com/ja-jp/library/cc428348.aspx
ペンを作っただけでは描画に反映されません
デバイスコンテキストにペンを割り当てなければなりません
SIZE(30){ COLOR(#ffaa00){描画オブジェクトをデバイスコンテキストに割り当てるにはSelectObject関数を使います}}
HPEN hDefPen = (HPEN)SelectObject(hdc,hPen);
他の描画オブジェクトに関しても割り当てないと反映されません
たとえば、ブラシなどは次のようにキャストして使います
HBRUSH hDefBrush = (HBRUSH)SelectObject(hdc,hBrush);
SelectObject関数の戻り値はHGDIOBJ型となっていますが、
これを割り当てた描画オブジェクト型にキャストします
ここで気を付けてほしいのはデフォルトの描画オブジェクトが戻り値として返ってきます
SIZE(30){ COLOR(#ffaa00){デフォルトの描画オブジェクトは描画終了時にデバイスコンテキストに戻さなければいけません}}
// 使い終わったら元の描画オブジェクトに戻す
SelectObject(hdc,hDefPen);
SelectObject(hdc,hDefBrush);
SelectObject(hBmpDC, hDefBmp);
SelectObject(hdc, hDefFont);
直線を描画するには、MoveTo関数で始点をセットし
LineTo関数で始点から終点までデバイスコンテキストに割り当てられたペンで描画します
(SelectObjectしなければデフォルトのペンで描画される)
// 直線描画
MoveToEx(hdc,始点x座標,始点y座標,NULL); // 始点をセット
LineTo(hdc,終点y座標,終点y座標); // 始点から割り当てられたペンで終点まで直線描画
続いて、曲線(ベジェ曲線)を描いてみます
ベジェ曲線とは端点(始点と終点)と制御点
を指定してやるだけで形状が決まる曲線です
3次元ベジェ曲線を描くためには最低4つの点が必要です
(3次元ベジェが最も一般的なベジェ曲線)
&ref(bezier.gif);
緑の枠に囲まれた部分を凸包と呼び、
曲線は必ず凸包の内部に収まるという性質があります
ベジェ曲線を描画するにはPolyBezier関数を使います
(SelectObjectしなければデフォルトのペンで描画される)
// 曲線描画
POINT pt[4] = {};
pt[0].x = 50;pt[0].y = 50;
pt[1].x = 100;pt[1].y = 150;
pt[2].x = 50;pt[2].y = 250;
pt[3].x = 150;pt[3].y = 250;
// 点情報から割り当てられたペンでベジェ曲線描画、点の数は3の倍数+1でなければならない
PolyBezier(hdc,点の配列,点の個数(3の倍数+1));
-----------------------------------------------------
続いてブラシを作ってみます
ブラシは図形の塗りつぶしに使います
単色の塗りつぶし用ブラシ作成にはCreateSolidBrush関数を使います
色を指定して作成します
// ブラシ作成
HBRUSH hBrush = CreateSolidBrush(RGB(0,255,0));
ここでは、説明しませんが
網目模様などのパターン描画にはCreateHatchBrush関数
画像のイメージをタイル状にしたブラシ作成にはCreatePatternBrush関数
などが用意されています
ブラシをデバイスコンテキストに割り当てた後
HBRUSH hDefBrush = (HBRUSH)SelectObject(hdc,hBrush);
図形を描画します
円(楕円)を描画するにはEllipse関数を使います
ペンで枠が描かれ
内部はブラシで塗りつぶしされます
(SelectObjectしなければデフォルトのペンとブラシで描画される)
枠が要らない場合は空のペンをSelectObject関数でデバイスコンテキストにセットしておきます
// 楕円描画
SelectObject(hdc,hNullPen);// 枠がいらない
RECT PointRect;
PointRect.left = -5;
PointRect.top = -5;
PointRect.right = 5;
PointRect.bottom = 5;
for(int i = 0;i < 4;++i)
Ellipse(hdc,pt[i].x + PointRect.left,pt[i].y + PointRect.top,pt[i].x + PointRect.right,pt[i].y + PointRect.bottom);
矩形を描画するには
Rectangle関数を使います
ペンで枠が描かれ
内部はブラシで塗りつぶしされます
(SelectObjectしなければデフォルトのペンとブラシで描画される)
枠が要らない場合は空のペンをSelectObject関数でデバイスコンテキストにセットしておきます
// 矩形描画
SelectObject(hdc,hNullPen); // 枠がいらない
RECT rect = {30,130,50,150};
Rectangle(hdc,rect.left,rect.top,rect.right,rect.bottom); // ブラシで指定された矩形を塗りつぶす
---------------------------------------------
ビットマップ画像(.bmp)読み込みです
読み込みにはLoadImage関数を使います
// ビットマップ読み込み
HINSTANCE hInstance = (HINSTANCE)GetModuleHandle(NULL); // インスタンスハンドル取得
HBITMAP hBmp = (HBITMAP)LoadImage(hInstance,_T("test.bmp"),IMAGE_BITMAP,0,0,LR_DEFAULTCOLOR | LR_SHARED | LR_LOADFROMFILE);
第一引数にはインスタンスハンドルを指定します
第二引数にはビットマップファイル名を指定します
第三引数には読み込みタイプを指定します(ビットマップ画像なのでIMAGE_BITMAP)
第四、五引数は0で構いません
最後の引数は読み込みフラグです(OR演算子「|」で組み合わせて使います)
ファイル読み込みなのでLR_LOADFROMFILEは必須です
LR_SHAREDは同じリソースなら、すでに読み込んであるビットマップハンドル(ポインタ)の使いまわしをします
LR_DEFAULTCOLORはカラー画像であることを示します(明示しなくてもカラーで読み込みします)
ビットマップハンドルが取得できたら
ビットマップの情報を取得します
BITMAP Bmp;
GetObject(hBmp,sizeof(BITMAP),&Bmp);
SIZE(30){ COLOR(#ffaa00){ GetObject関数で描画オブジェクトの情報を取得できます}}
ペンやブラシ、フォントに関しても次のように取得できます
// ペン
LOGPEN pen;
GetObject(hPen,sizeof(LOGPEN),&pen);
// ブラシ
LOGBRUSH brush;
GetObject(hPen,sizeof(LOGBRUSH),&brush);
// フォント
LOGFONT font;
GetObject(hFont,sizeof(LOGFONT),&font);
GetObjectしたBITMAP構造体のbmWidth,bmHeightに画像サイズが入っています
ビットマップの情報を読み取った後
描画画面と互換性を持つ裏画面を作成します
これはあとで画像を描画するために必要です
HDC hBmpDC = CreateCompatibleDC(hdc); // 裏画面を作成します
HBITMAP hDefBmp = (HBITMAP)SelectObject(hBmpDC, hBmp);
ここで気を付けて欲しいのはSelectObject関数で画像を裏画面と対応づけることです
直接画像を描画することができないので
裏画面から描画画面へ画像を転送します
画像を転送するにはBitBlt関数を使います
// 画像表示
BitBlt(転送先デバイスコンテキスト,転送先X座標,転送先Y座標,画像幅,画像高さ,転送元デバイスコンテキスト,転送元X座標,転送元Y座標,ラスタオペレーションコード);
BitBlt(hdc,200,0,Bmp.bmWidth,Bmp.bmHeight,hBmpDC,0,0,SRCCOPY);
第一引数は描画画面のデバイスコンテキストを指定します
第二、三引数は描画する位置を指定します
第四、五引数は描画サイズを指定します
ここで注意してほしいのは画像を縮小拡大して転送するのではありません
画像サイズより小さいサイズを指定した場合、そのサイズだけ切り取られます
そのままコピーしたい場合はそのままの画像サイズを指定してやります
第六引数は転送元デバイスコンテキストは裏画面のデバイスコンテキストを指定します
転送元X座標、転送元Y座標は
画像をそのままコピーしたい場合は0を指定してやればいいのですが
(というのは裏画面の左上隅に画像はセットされている)
いまひとつわかりにくいと思います
次の図を見てください
&ref(bitblt.GIF);
画像幅、画像高さ、転送元X、転送元Yを
うまく指定してやれば画像の一部を切り取ることができます
アニメーション用の画像などに利用できます
ラスタオペレーションコードはどのように転送するかのフラグですSRCCOPYを指定するとそのまま画像が描画画面にコピーされます
----------------------------------------------------------
続いて文字列描画です
フォントを作成するには
CreateFont関数もしくはCreateFontIndirect関数を使います
CreateFont関数は引数があまりにも多いのでやめました
今回はCreateFontIndirect関数を使います
引数の代わりにLOGFONT構造体でフォントの情報を指定しなければなりません
(こちらも変数多いのですが・・・)
// フォント作成
LOGFONT logfont = {};
logfont.lfHeight = 30; // 文字高さ
logfont.lfWidth = 12; // 文字幅
logfont.lfEscapement = 30; // 文字送りの方向とX軸の角度
logfont.lfOrientation = logfont.lfEscapement; // ベースラインとX軸との角度
logfont.lfWeight = FW_BOLD; // フォントの太さ
logfont.lfItalic = TRUE; // イタリック体指定
logfont.lfUnderline = TRUE; // 下線付き指定
logfont.lfStrikeOut = TRUE; // 打ち消し線指定
logfont.lfCharSet = SHIFTJIS_CHARSET; // キャラクタセット
logfont.lfOutPrecision = OUT_DEFAULT_PRECIS; // 出力精度
logfont.lfClipPrecision = CLIP_DEFAULT_PRECIS; // クリッピングの精度
logfont.lfQuality = DEFAULT_QUALITY; // 出力品質
logfont.lfPitchAndFamily= FF_DONTCARE|DEFAULT_PITCH; // ファミリー
_tcscpy(logfont.lfFaceName,_T("MS ゴシック")); // フォント名
HFONT hFont = CreateFontIndirect(&logfont);
HFONT hDefFont = (HFONT)SelectObject(hdc,hFont);
LOGFONT構造体変数で
とりあえずよく使うやつだけ説明します
それ以外はとりあえずはそのまま使ってください
lfHeight は文字高さを指定します
lfWidth は文字幅を指定します
lfEscapementは文字角度を指定します。(1/10度指定です)傾けたくないときは0を指定します。
lfOrientationはlfEscapementと同じものを指定してください
lfWeightは文字の太さを決めます
lfItalicはイタリック表示です(使わない場合はFALSE)
lfUnderlineは文字の下に下線を引きます(使わない場合はFALSE)
lfStrikeOutは文字に打ち消し線を引きます(使わない場合はFALSE)
lfFaceNameはフォント名を指定します(MS ゴシック、MS 明朝など)
SetTextColor関数で文字の色を変更し
TextOut関数で文字列を描画します
(SelectObjectしなければデフォルトのフォントで描画される)
最後の引数は文字数指定します
_tcslen関数はstrlenと_wstrlenを切り変えるマクロです
// 文字列描画
const TCHAR* szText = _T("Hello World");
SetTextColor(hdc,RGB(0,0,255)); // 文字色を青に変更
TextOut(hdc,X,Y,szText,(int)_tcslen(szText)); // 文字列描画
----------------------------------------------------------
最後に、
SIZE(30){ COLOR(#ffaa00){ 新しく作成(Createなんとか)した描画オブジェクトとデバイスコンテキストは解放しなければなりません}}
しないとハンドルリークを起こします(メモリリークの一種)
// 使わなくなった描画オブジェクトは破棄
DeleteObject(hPen);
DeleteObject(hNullPen);
DeleteObject(hBrush);
DeleteObject(hBmp);
DeleteObject(hDefFont);
// 使わなくなったデバイスコンテキストを破棄
DeleteDC(hBmpDC);
描画オブジェクトに関してはDeleteObject関数
デバイスコンテキストに関してはDeleteDC関数
を使います
さらに詳しい対応関係はMSDN公式を参照してください
http://msdn.microsoft.com/en-us/library/ms724291%28VS.85%29.aspx
-------------------------------------------------------
ふぅ・・・これで基本的な描画の説明は終わりました(^^;:)
ここで全てを取り上げるの量的に不可能なので
その他の描画に関しての情報はMSDN公式を見てください
MSDN公式
http://msdn.microsoft.com/ja-jp/library/cc428835.aspx
しかし、冗長なコードですね
やれることが多いのはいいのですが一から描くと
描画オブジェクトの設定が非常にめんどくさいです
GDI+を使えば、純粋に描くよりかなり遅いですが楽に描画できます
また、ビットマップ以外の画像も簡単に扱うことができます
ただ、裏側で何が行われているか知るためにもこの回は重要です
全ソースコードは下から
&ref(main.cpp);
プログラムで使う画像はこちら
&ref(test.bmp);
画像は作成したプロジェクトと同じ場所においてください
&ref(projectfolder.GIF);
#vote((^ω^)やったお[2],何これwww意味不すぎwww[1],。(`ω´#)。あぁん?最近、だらしねぇな[0])
#vote((^ω^)やったお[3],何これwww意味不すぎwww[1],。(`ω´#)。あぁん?最近、だらしねぇな[0])