MFCでの基本的な描画処理(GDI)

MFC

MFCでGUIの開発を行っている際に「図形の描画をしたい」というタイミングがたまにあります。

図形の描画はプログラミング言語やフレームワークの種類によって書き方が異なるため、文法や構文とは違い他の言語を触っていたからといって「なんとなく書ける」ということはなかなかありません。

MFCにおける描画処理は「GDI」を使用したものと「GDI+」を使用したものとがありますが、この記事ではGDIを使用した基本的な描画処理の方法について解説します。

MFCにおける描画処理を記述する場所

MFCにおける描画処理は基本的には「OnPaint」「DrawItem」などの描画イベントから呼び出される処理の中に記述する必要があります。

描画処理を学び始めたばかりの型の中には、「ボタンを押したタイミングで描画」「タイマーで特定の周期で描画」といったように、描画イベント外で描画処理を記述してしまう方がいます。

描画イベント外で描画処理を実行してしまうと、再描画イベントが発生した際にデフォルトの描画処理で上書きされてしまうため、対象のウィンドウやコントロールがディスプレイ外にはみ出た場合や他のウィンドウが重なった場合などに意図しない挙動となることがあります。

void CHogeWnd::OnPaint()
{
    if (this->GetSafeHwnd())
    {
        // この中か、ここから呼び出される関数内で描画処理を記述する
    }
}

以降で解説する描画処理は全て描画イベント以降で実施するようにしてください。

基本的な描画処理

本記事では以下の5つの図形(+α)の描画方法について解説します。

  • 線分
  • 矩形(四角形)
  • 角丸矩形(角丸四角形)
  • 文字列

デバイスコンテキストの取得

GDIで描画処理を行う場合には全てデバイスコンテキストに対して処理を行います。

ウィンドウクラスやコントロールクラスなどのCWndを派生したクラスは全てデバイスコンテキストを保持しているため、処理を行う際には以下の方法でデバイスコンテキストを取得します。

// デバイスコンテキスト取得
CDC* pDC = this->GetDC();

描画の設定と注意点

図形を描画する際には事前に枠線や塗りつぶしの設定を行います。

また、それらの設定を行った場合には後処理(オブジェクトの開放など)を行わないとリソースリークという異常終了が発生することがあるため注意が必要です。

(枠)線の設定

(枠)線の設定では、「線分」「矩形」「角丸矩形」「円」の「線種」「太さ」「色」を設定します。

ペンオブジェクトを作成して、作成したオブジェクトをデバイスコンテキストに設定することで描画時の(枠)線の情報に反映されます。

// ペン
CPen pen(PS_SOLID, 1, RGB(0, 0, 0));
// デバイスコンテキストにオブジェクトを設定
CPen* pOldPen = pDC->SelectObject(&pen);

ペンオブジェクトのコンストラクタには「線種(下表参照)」「太さ」「色(COLORREF型)」を指定します。

PS_SOLID実線
PS_DASH破線
PS_DOT点線
PS_DASHDOT1点破線
PS_NULL
PS_INSIDEFRAME実線、指定された枠内に線が収まる

塗り潰しの設定

塗り潰しの設定では、「矩形」「角丸矩形」「円」の「塗り潰し色」を設定します。

ブラシオブジェクトを作成して、作成したオブジェクトをデバイスコンテキストに設定することで描画時の塗り潰しの情報に反映されます。

// ブラシ
CBrsuh brs(RGB(255, 255, 0));
// デバイスコンテキストにオブジェクトを設定
CBrush* pOldBrs = pDC->SelectObject(&brs);

また、枠線のみ描画して塗り潰しはしたくない場合には、以下のように空のブラシを取得して設定します。

空のブラシはオブジェクトではなくハンドルであるため、SelectObjectにアドレスを渡さないように注意が必要です。

// 空のブラシ
HGDIOBJ hBrs = ::GetStockObject(NULL_BRUSH);
// デバイスコンテキストにオブジェクトを設定
HGDIOBJ hOldBrs = pDC->SelectObject(hBrs);

後処理

先述した通りペンやブラシなどのオブジェクトは使用が終了したら破棄する必要があります。

破棄をせずに生成し続けるといずれリソースリークとなりアプリケーションが異常終了することになります。

慣れていない方は異常終了の原因をリソースリークと特定するのが難しいと思うので、描画処理終了後にしっかりと後処理を行うようにしましょう。

// オブジェクトを元に戻す
pDC->SelectObject(pOldPen);  // ペンを使用した場合
pDC->SelectObject(pOldBrs);  // ブラシを使用した場合
pDC->SelectObject(hOldBrs);  // 空のブラシを使用した場合
// オブジェクトの破棄
pen.DeleteObject();          // ペンを使用した場合
brs.DeleteObject();          // ブラシを使用した場合

「オブジェクトを元に戻す」で指定している引数は、(枠)線の設定や塗り潰しの設定でデバイスコンテキストにオブジェクトを設定した際の戻り値を使用しており、これは設定前のオブジェクトです。

図形(+α)の描画

線分の描画

線分の描画は「MoveTo」「LineTo」関数を使用して実施します。

「MoveTo」関数で始点位置の設定、「LineTo」関数で終点一の設定と同時に始点位置から終点位置まで線分の描画を行います。

// ペン(線種、太さ、色の指定)
CPen pen(PS_SOLID, 1, RGB(0, 0, 0));
// デバイスコンテキストにペンを設定
CPen* pOldPen = pDC->SelectObject(&pen);
// 始点位置設定
pDC->MoveTo(0, 0);
// 終点位置まで描画
pDC->LineTo(100, 100);
// ペンを元に戻す
pDC->SelectObject(pOldPen);
// ペンの破棄
pen.DeleteObject();

線種や太さを変えて描画をすると以下のように線分が描画されます。

矩形(四角形)の描画

矩形の描画は「Rectangle」関数を使用して実施します。

「Rectangle」関数に矩形(CRect)を指定することで、設定された枠線情報・塗り潰し情報で矩形の描画を行います。

また、矩形の描画に関しては枠線なし・塗り潰しありの場合の関数として「FillSolidRect」関数が用意されているため、「FillSolidRect」関数に矩形と色を指定して矩形の描画を行います。

// 矩形の描画(枠線あり、塗り潰しあり)

// ペン(線種、太さ、色の指定)
CPen pen(PS_SOLID, 1, RGB(0, 0, 0));
// ブラシ(塗り潰し色の指定)
CBrush brs(RGB(255, 255, 255));
// デバイスコンテキストにオブジェクトを設定
CPen* pOldPen = pDC->SelectObject(&pen);
CBrush* pOldBrs = pDC->SelectObject(&brs);
// 矩形描画
pDC->Rectangle(CRect(0, 0, 100, 100));
// オブジェクトを元に戻す
pDC->SelectObject(pOldPen);
pDC->SelectObject(pOldBrs);
// オブジェクトの破棄
pen.DeleteObject();
brs.DeleteObject();
// 矩形の描画(枠線なし、塗り潰しあり)

// 矩形描画
pDC->FillSolidRect(CRect(0, 0, 100, 100), RGB(255, 255, 255));
// 矩形の描画(枠線あり、塗り潰しなし)

// ペン(線種、太さ、色の指定)
CPen pen(PS_SOLID, 1, RGB(0, 0, 0));
// 空のブラシ
HGDIOBJ hBrs = ::GetStockObject(NULL_BRUSH);
// デバイスコンテキストにオブジェクトを設定
CPen* pOldPen = pDC->SelectObject(&pen);
HGDIOBJ hOldBrs = pDC->SelectObject(hBrs);
// 矩形描画
pDC->Rectangle(CRect(0, 0, 100, 100));
// オブジェクトを元に戻す
pDC->SelectObject(pOldPen);
pDC->SelectObject(hOldBrs);
// オブジェクトの破棄
pen.DeleteObject();

それぞれ色や太さを変えて描画をすると以下のように矩形が描画されます。

角丸矩形(角丸四角形)の描画

角丸矩形の描画は「RoundRect」関数を使用して実施します。

「RoundRect」関数に矩形(CRect)と角丸半径(CPoint)を指定することで、設定された枠線情報・塗り潰し情報で角丸矩形の描画を行います。

// 角丸矩形の描画(枠線あり、塗り潰しあり)

// ペン(線種、太さ、色の指定)
CPen pen(PS_SOLID, 1, RGB(0, 0, 0));
// ブラシ(塗り潰し色の指定)
CBrush brs(RGB(255, 255, 255));
// デバイスコンテキストにオブジェクトを設定
CPen* pOldPen = pDC->SelectObject(&pen);
CBrush* pOldBrs = pDC->SelectObject(&brs);
// 角丸矩形描画
pDC->RoundRect(CRect(0, 0, 100, 100), CPoint(20, 20));
// オブジェクトを元に戻す
pDC->SelectObject(pOldPen);
pDC->SelectObject(pOldBrs);
// オブジェクトの破棄
pen.DeleteObject();
brs.DeleteObject();
// 角丸矩形の描画(枠線なし、塗り潰しあり)

// ペン(線種、太さ、色の指定)
CPen pen(PS_NULL, 0, RGB(0, 0, 0));
// ブラシ(塗り潰し色の指定)
CBrush brs(RGB(255, 255, 255));
// デバイスコンテキストにオブジェクトを設定
CPen* pOldPen = pDC->SelectObject(&pen);
CBrush* pOldBrs = pDC->SelectObject(&brs);
// 角丸矩形描画
pDC->RoundRect(CRect(0, 0, 100, 100), CPoint(20, 20));
// オブジェクトを元に戻す
pDC->SelectObject(pOldPen);
pDC->SelectObject(pOldBrs);
// オブジェクトの破棄
pen.DeleteObject();
brs.DeleteObject();
// 角丸矩形の描画(枠線あり、塗り潰しなし)

// ペン(線種、太さ、色の指定)
CPen pen(PS_SOLID, 1, RGB(0, 0, 0));
// 空のブラシ
HGDIOBJ hBrs = ::GetStockObject(NULL_BRUSH);
// デバイスコンテキストにオブジェクトを設定
CPen* pOldPen = pDC->SelectObject(&pen);
HGDIOBJ hOldBrs = pDC->SelectObject(hBrs);
// 角丸矩形描画
pDC->RoundRect(CRect(0, 0, 100, 100), CPoint(20, 20));
// オブジェクトを元に戻す
pDC->SelectObject(pOldPen);
pDC->SelectObject(hOldBrs);
// オブジェクトの破棄
pen.DeleteObject();

それぞれ色や太さを変えて描画をすると以下のように角丸矩形が描画されます。

円の描画

円の描画は「Ellipse」関数を使用して実施します。

「Ellipse」関数に矩形(CRect)を指定することで、設定された枠線情報・塗り潰し情報で円の描画を行います。

// 円の描画(枠線あり、塗り潰しあり)

// ペン(線種、太さ、色の指定)
CPen pen(PS_SOLID, 1, RGB(0, 0, 0));
// ブラシ(塗り潰し色の指定)
CBrush brs(RGB(255, 255, 255));
// デバイスコンテキストにオブジェクトを設定
CPen* pOldPen = pDC->SelectObject(&pen);
CBrush* pOldBrs = pDC->SelectObject(&brs);
// 円描画
pDC->Ellipse(CRect(0, 0, 100, 100));
// オブジェクトを元に戻す
pDC->SelectObject(pOldPen);
pDC->SelectObject(pOldBrs);
// オブジェクトの破棄
pen.DeleteObject();
brs.DeleteObject();
// 円の描画(枠線なし、塗り潰しあり)

// ペン(線種、太さ、色の指定)
CPen pen(PS_NULL, 0, RGB(0, 0, 0));
// ブラシ(塗り潰し色の指定)
CBrush brs(RGB(255, 255, 255));
// デバイスコンテキストにオブジェクトを設定
CPen* pOldPen = pDC->SelectObject(&pen);
CBrush* pOldBrs = pDC->SelectObject(&brs);
// 円描画
pDC->Ellipse(CRect(0, 0, 100, 100));
// オブジェクトを元に戻す
pDC->SelectObject(pOldPen);
pDC->SelectObject(pOldBrs);
// オブジェクトの破棄
pen.DeleteObject();
brs.DeleteObject();
// 円の描画(枠線あり、塗り潰しなし)

// ペン(線種、太さ、色の指定)
CPen pen(PS_SOLID, 1, RGB(0, 0, 0));
// 空のブラシ
HGDIOBJ hBrs = ::GetStockObject(NULL_BRUSH);
// デバイスコンテキストにオブジェクトを設定
CPen* pOldPen = pDC->SelectObject(&pen);
HGDIOBJ hOldBrs = pDC->SelectObject(hBrs);
// 円描画
pDC->Ellipse(CRect(0, 0, 100, 100));
// オブジェクトを元に戻す
pDC->SelectObject(pOldPen);
pDC->SelectObject(hOldBrs);
// オブジェクトの破棄
pen.DeleteObject();

それぞれ色や太さを変えて描画をすると以下のように円が描画されます。

文字列の描画

文字列の描画は「DrawText」関数を使用して実施します。

「DrawText」関数に矩形(CRect)、文字列(CString)、フォーマットを指定することで文字列の描画を行うことができます。

文字列の描画を行う場合にも、「SetTextColor」関数や「SetBkMode」関数を使用してあらかじめ「文字色」「文字背景色」「フォント」などの設定を行います。

描画時に設定するフォーマットは以下のフラグを必要に応じて指定します。(以下はよく使われるもの)

DT_TOP上寄せ
DT_LEFT左寄せ
DT_CENTER中央寄せ(水平方向)
DT_RIGHT右寄せ
DT_VCENTER中央寄せ(垂直方向)、「DT_SINGLELINE」指定時のみ有効
DT_BOTTOM下寄せ、「DT_SINGLELINE」指定時のみ有効
DT_SINGLELINE一行描画
// 文字色の設定
COLORREF oldColor = pDC->SetTextColor(RGB(0, 0, 0));
// 文字背景モードの設定
int iOldBkMode = pDC->SetBkMode(TRANSPARENT);
// 文字列描画
pDC->DrawText(_T("文字列の描画"), CRect(0, 0, 400, 100), DT_LEFT | DT_SINGLELINE | DT_VCENTER);
// 設定を元に戻す
pDC->SetTextColor(oldColor);
pDC->SetBkMode(iOldBkMode);

適当な設定を行って描画をすると以下のように文字列が描画されます。

終わりに

MFCにおけるGDIを使用した基本的な描画処理の方法について解説しました。

リソースリークが発生すると原因の特定が難しいことが多いので、しっかりとGDIを使用した描画の方法を覚えておいたほうがよいです。

この記事について誤っている点・不明な点などありましたらコメントまでお願いします。

コメント

タイトルとURLをコピーしました