MFCで描画処理を行う方法(GDI編) ダブルバッファリングから基本的な描画処理まで

MFCではGDIGDI+という2つの手法描画処理を行うことができます。

この記事では、GDIでの描画を行う方法について解説します。

描画処理の基本

GDIで描画処理を行う場合には、描画メッセージを受け取る場所以降のパスに、ウィンドウやコントロールのデバイスコンテキストに対して描画に関する処理を記述します。

描画メッセージを受け取る場所は基本的にはOnPaint関数となり、OnPaint関数以降のパスに描画処理を記述していくことになります。

MFC
// CHogeWndコントロールの描画メッセージ関数
void CHogeWnd::OnPaint()
{
    // デフォルトで記述されている以下の一文は削除しない
    CPaintDC dc(this);

    if(this->GetSafeHwnd())
    {
        // 以降に独自の描画処理を記述する

    }
}

詳細な描画処理の記述方法については以降で解説します。

ダブルバッファリング

デバイスコンテキストに対して描画処理を行う場合、高頻度で描画処理を行う場合にはチラツキが発生することがあります。

チラツキは、重ね描きをしたりする場合に先に描いたものが一瞬表示されてしまうことなどが原因となります。

チラツキを抑止する方法として一般的に用いられるのはダブルバッファリングという手法で、これはメモリ上に仮想のキャンバス(MFCではデバイスコンテキスト)を用意し、そこに全て描画を行ってから本物のキャンバスにコピーをすることでチラツキの抑制をします。

MFC
// 描画処理を行う関数
void CHogeWnd::DrawObject()
{
    // 実デバイスコンテキスト
    CDC* pDC = this->GetDC();
    // 仮想デバイスコンテキスト
    CDC memDC;
    // 仮想デバイスコンテキスト用ビットマップ
    CBitmap memBmp;
    // 矩形
    CRect rc;

    // 矩形取得
    this->GetClientRect(rc);
    // 仮想デバイスコンテキスト生成
    memDC.CreateCompatibleDC(pDC);
    // 仮想デバイスコンテキスト用ビットマップ生成
    memBmp.CreateCompatibleBitmap(pDC, rc.Width(), rc.Height());
    // 仮想デバイスコンテキストにビットマップ割り当て
    memDC.SelectObject(&memBmp);

    // ↓ここから描画処理↓

    // ↑ここまで描画処理↑

    // 実デバイスコンテキストに貼り付け
    pDC->BitBlt(rc.left, rc.top, rc.Width(), rc.Height(), &memDC, 0, 0, SRCCOPY);
    // オブジェクト破棄
    ::DeleteObject(memDC);
    ::DeleteObject(memBmp);
    this->ReleaseDC(pDC);
}

後述する描画処理は、上記関数の「↓ここから描画処理↓」から「↑ここまで描画処理↑」の間に記述していきます。

また、ダブルバッファリングをしているのに描画処理を実デバイスコンテキストに対して行う方がたまにいますが、描画処理は仮想デバイスコンテキストに対して行うようにしてください。

基本的な図形の描画処理

ここからは実際の描画処理方法について解説します。

描画処理でよく使われるのは以下の図形(+α)です。

  • 線分
  • 矩形
  • 角丸矩形
  • 文字列

「矩形」「角丸矩形」「円」に関しては、枠線の有無・塗り潰しの有無があるため、それぞれさらに3つに分類します。

それぞれ以降で実際のコードを説明します。

線分の描画

線分の描画を行う場合には、MoveTo関数とLineTo関数により始点と終点を設定します。

また、線種や太さなどを設定するためのペンの設定を行います。

MFC
// 線分の描画
void CHogeWnd::DrawLine(CDC* pDC, CPoint pt1, CPoint pt2, int width, COLORREF color)
{
    // 線分用ペン
    CPen pen(PS_SOLID, width, color);
    // ペンをデバイスコンテキストに設定
    pDC->SelectObject(&pen);
    // 始点設定
    pDC->MoveTo(pt1);
    // 終点設定
    pDC->LineTo(pt2);
}

上記関数を呼び出す場合には、以下のように記述します。

MFC
// 線分の描画
this->DrawLine(&memDC, CPoint(10, 10), CPoint(110, 110), 2, RGB(255, 255, 0));

実行すると、以下のようになります。

矩形の描画

矩形の描画を行う場合には、Rectangle関数により矩形情報を設定します。

また、枠線の情報を設定するためのペンと塗り潰しのためのブラシの設定も行います。

MFC
// 矩形の描画(枠線有、塗り潰し有)
void CHogeWnd::DrawRect(CDC* pDC, CRect rc, int width, COLORREF borderColor, COLORREF fillColor)
{
    // 枠線用ペン
    CPen pen(PS_SOLID, width, borderColor);
    // 塗り潰し用ブラシ
    CBrush brs(fillColor);
    // ペンをデバイスコンテキストに設定
    pDC->SelectObject(&pen);
    // ブラシをデバイスコンテキストに設定
    pDC->SelectObject(&brs);
    // 矩形情報設定
    pDC->Rectangle(rc);
}

枠線なしの矩形を描画する場合には、ペンやブラシの設定をすることなくFillSolidRect関数だけでそのまま実現できます。

MFC
// 矩形の描画(枠線なし、塗り潰し有)
void CHogeWnd::DrawRect(CDC* pDC, CRect rc, COLORREF fillColor)
{
    // 矩形の描画
    pDC->FillSolidRect(rc, fillColor);
}

塗り潰しなしの矩形を描画する場合には、ブラシの代わりにGetStockObject(NULL_BRUSH)により取得できる透明ブラシを使います。

MFC
// 矩形の描画 (枠線有、塗り潰しなし)
void CHogeWnd::DrawRect(CDC* pDC, CRect rc, int width, COLORREF borderColor)
{
    // 枠線用ペン
    CPen pen(PS_SOLID, width, borderColor);
    // 透明ブラシ
    HGDIOBJ hBrs = ::GetStockObject(NULL_BRUSH);
    // ペンをデバイスコンテキストに設定
    pDC->SelectObject(&pen);
    // ブラシをデバイスコンテキストに設定
    pDC->SelectObject(hBrs);
    // 矩形情報設定
    pDC->Rectangle(rc);
}

上記の3つの関数を呼び出す場合には、以下のように記述します。

MFC
// 矩形の描画(枠線有、塗り潰し有)
this->DrawRect(&memDC, CRect(10, 10, 60, 60), 1, RGB(255, 0, 0), RGB(255, 255, 0));
// 矩形の描画(枠線なし、塗り潰し有)
this->DrawRect(&memDC, CRect(70, 10, 120, 60), RGB(0, 255, 0));
// 矩形の描画(枠線有、塗り潰しなし)
this->DrawRect(&memDC, CRect(130, 10, 180, 60), 1, RGB(0, 0, 255));

実行すると、以下のようになります。

角丸矩形の描画

角丸矩形の描画を行う場合には、RoundRect関数により矩形情報と角丸のサイズを設定します。

その他については、矩形の描画とほぼ全て同じです。(FillSolidRect関数の角丸矩形版はないので枠線サイズを0とします)

MFC
// 角丸矩形の描画(枠線有、塗り潰し有)
void CHogeWnd::DrawRoundRect(CDC* pDC, CRect rc, CPoint r, int width, COLORREF borderColor, COLORREF fillColor)
{
    // 枠線用ペン
    CPen pen(PS_SOLID, width, borderColor);
    // 塗り潰し用ブラシ
    CBrush brs(fillColor);
    // ペンをデバイスコンテキストに設定
    pDC->SelectObject(&pen);
    // ブラシをデバイスコンテキストに設定
    pDC->SelectObject(&brs);
    // 角丸矩形情報設定
    pDC->RoundRect(rc, r);
}
MFC
// 角丸矩形の描画(枠線なし、塗り潰し有)
void CHogeWnd::DrawRoundRect(CDC* pDC, CRect rc, CPoint r, COLORREF fillColor)
{
    // サイズ0のペン
    CPen pen(PS_SOLID, 0, fillColor);
    // 塗り潰し用ブラシ
    CBrush brs(fillColor);
    // ペンをデバイスコンテキストに設定
    pDC->SelectObject(&pen);
    // ブラシをデバイスコンテキストに設定
    pDC->SelectObject(&brs);
    // 角丸矩形情報設定
    pDC->RoundRect(rc, r);
}
MFC
// 角丸矩形の描画(枠線有、塗り潰しなし)
void CHogeWnd::DrawRoundRect(CDC* pDC, CRect rc, CPoint r, int width, COLORREF borderColor)
{
    // 枠線用ペン
    CPen pen(PS_SOLID, width, borderColor);
    // 透明ブラシ
    HGDIOBJ hBrs = ::GetStockObject(NULL_BRUSH);
    // ペンをデバイスコンテキストに設定
    pDC->SelectObject(&pen);
    // ブラシをデバイスコンテキストに設定
    pDC->SelectObject(hBrs);
    // 角丸矩形情報設定
    pDC->RoundRect(rc, r);
}

上記の3つの関数を呼び出す場合には、以下のように記述します。

MFC
// 角丸矩形の描画(枠線有、塗り潰し有)
this->DrawRoundRect(&memDC, CRect(10, 10, 60, 60), CPoint(10, 10), 1, RGB(255, 0, 0), RGB(255, 255, 0));
// 角丸矩形の描画(枠線なし、塗り潰し有)
this->DrawRoundRect(&memDC, CRect(70, 10, 120, 60), CPoint(10, 10), RGB(0, 255, 0));
// 角丸矩形の描画(枠線有、塗り潰しなし)
this->DrawRoundRect(&memDC, CRect(130, 10, 180, 60), CPoint(10, 10), 1, RGB(0, 0, 255));

実行すると、以下のようになります。

円の描画

円の描画を行う場合には、Ellipse関数によって円の矩形情報を設定します。

その他については、矩形の描画とほぼ全て同じです。(FillSolidRect関数の角丸矩形版はないので枠線サイズを0とします)

MFC
// 円の描画(枠線有、塗り潰し有)
void CHogeWnd::DrawEllipse(CDC* pDC, CRect rc, int width, COLORREF borderColor, COLORREF fillColor)
{
    // 枠線用ペン
    CPen pen(PS_SOLID, width, borderColor);
    // 塗り潰し用ブラシ
    CBrush brs(fillColor);
    // ペンをデバイスコンテキストに設定
    pDC->SelectObject(&pen);
    // ブラシをデバイスコンテキストに設定
    pDC->SelectObject(&brs);
    // 矩形情報設定
    pDC->Ellipse(rc);
}
MFC
// 円の描画(枠線なし、塗り潰し有)
void CHogeWnd::DrawEllipse(CDC* pDC, CRect rc, COLORREF fillColor)
{
    // サイズ0のペン
    CPen pen(PS_SOLID, 0, fillColor);
    // 塗り潰し用ブラシ
    CBrush brs(fillColor);
    // ペンをデバイスコンテキストに設定
    pDC->SelectObject(&pen);
    // ブラシをデバイスコンテキストに設定
    pDC->SelectObject(&brs);
    // 矩形情報設定
    pDC->Ellipse(rc);
}
MFC
// 円の描画(枠線有、塗り潰しなし)
void CHogeWnd::DrawEllipse(CDC* pDC, CRect rc, int width, COLORREF borderColor)
{
    // 枠線用ペン
    CPen pen(PS_SOLID, width, borderColor);
    // 透明ブラシ
    HGDIOBJ hBrs = ::GetStockObject(NULL_BRUSH);
    // ペンをデバイスコンテキストに設定
    pDC->SelectObject(&pen);
    // ブラシをデバイスコンテキストに設定
    pDC->SelectObject(hBrs);
    // 矩形情報設定
    pDC->Ellipse(rc);
}

上記の3つの関数を呼び出す場合には、以下のように記述します。

MFC
// 円の描画(枠線有、塗り潰し有)
this->DrawEllipse(&memDC, CRect(10, 10, 60, 60), 1, RGB(255, 0, 0), RGB(255, 255, 0));
// 円の描画(枠線なし、塗り潰し有)
this->DrawEllipse(&memDC, CRect(70, 10, 120, 60), RGB(0, 255, 0));
// 円の描画(枠線有、塗り潰しなし)
this->DrawEllipse(&memDC, CRect(130, 10, 180, 60), 1, RGB(0, 0, 255));

実行すると、以下のようになります。

文字列の描画

文字列の描画を行う場合には、DrawText関数によって描画する文字列と矩形を設定します。

また、SetTextColor関数で文字色を、SetBkColor関数で文字背景色を設定します。

ここでは、SetBkMode(TRANSPARENT)として文字背景色は透過にします。

MFC
// 文字列の描画
void CHogeWnd::DrawText(CDC* pDC, CRect rc, CString text, COLORREF textColor, UINT format)
{
    // 文字色設定
    pDC->SetTextColor(textColor);
    // 文字背景透過
    pDC->SetBkMode(TRANSPARENT);
    // フォント設定(ここではコントロールフォント使用)
    pDC->SelectObject(this->GetFont());
    // 文字列描画
    pDC->DrawText(text, rc, format);
}

上記の関数を呼び出す場合には、以下のように記述します。

MFC
// 文字列の描画
this->DrawText(&memDC, CRect(10, 10, 110, 40), _T("GDI Drawing!"), RGB(255, 255, 0), DT_LEFT | DT_SINGLELINE | DT_VCENTER);

実行すると、以下のようになります。

アンチエイリアスについて

ここまで紹介してきた方法では、すでに気づいている方もいるかもしれませんが曲線等にジャギー(境目のギザギザ)が発生してしまいます。

しかし、GDIによる描画ではアンチエイリアスを行うことができないため、描画処理の選定時にアンチエイリアスが必須であることがわかっている場合にはGDIではなくGDI+を選択するよう注意してください。

GDI+によるアンチエイリアスを伴った描画処理については、いずれ記事にしますので乞うご期待!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です