【MFC】AlphaBlendで半透明に重ねて描く:乗算済みアルファとAC_SRC_ALPHA・SourceConstantAlphaの使い分け

【MFC】AlphaBlendで半透明に重ねて描く:乗算済みアルファとAC_SRC_ALPHA・SourceConstantAlphaの使い分け

MFCで画像を半透明に重ねるときは、CDC::AlphaBlendを使います。画像全体を同じ濃さで薄くするならBLENDFUNCTIONのSourceConstantAlphaだけで足りますが、丸い形だけを重ねたり縁をぼかしたりするには、画素ごとのアルファを持った32ビットのビットマップとAC_SRC_ALPHAが要ります。このとき色にアルファを掛けておかないと縁に元の絵に無い色が出て、アルファを入れたあとにGDIで描いた所はアルファが0になり、黒い文字なら抜けて見えます。


目次

AlphaBlendの6通りの描き方と結果

上半分が明るく下半分が暗い格子の上に、縁をぼかした黄色の警告マーク(72×72ピクセル、中に黒い「!」)を6通りに描くと、次のようになります。

AlphaBlendで警告マークを6通りに重ねた画面。2は円だけが縁をぼかして描かれ、3は縁に紫と緑の輪が出て、5は「!」の所に背景が見え、6は何も描かれていない
2は円だけが縁をぼかして重なる。3は縁に輪、5は「!」が抜け、6は何も出ない
番号ビットマップBLENDFUNCTION結果
1CreateCompatibleBitmapに白い四角・黄色の円・「!」を描いた普通のビットマップSourceConstantAlpha 128、AlphaFormat 0白い四角ごと半分の濃さになる
232ビットDIBにGDIで色と「!」を描き、そのあと画素ごとにアルファを入れて色に掛けたSourceConstantAlpha 255、AC_SRC_ALPHA円だけが描かれ、縁がぼける
32と同じだが、色にアルファを掛けていないSourceConstantAlpha 255、AC_SRC_ALPHA縁に紫と緑の輪が出る
42と同じSourceConstantAlpha 128、AC_SRC_ALPHA円の形のまま全体が薄くなる
532ビットDIBにアルファを入れて色に掛けたあとで、GDIで「!」を描いたSourceConstantAlpha 255、AC_SRC_ALPHA「!」の所が抜けて背景が見える
61と同じ普通のビットマップSourceConstantAlpha 255、AC_SRC_ALPHA何も描かれない

画像全体を薄くするなら1、形に沿って重ねるなら2の描き方にします。


画像全体を薄くするならSourceConstantAlphaだけでよい

BLENDFUNCTIONのSourceConstantAlphaに0〜255の濃さを入れ、AlphaFormatは0にします。

void CMonitorDlg::DrawFaded(CDC* pDC, int x, int y, CBitmap* pBitmap, int cx, int cy)
{
    CDC dcSrc;
    dcSrc.CreateCompatibleDC(pDC);
    CBitmap* pOldBitmap = dcSrc.SelectObject(pBitmap);

    BLENDFUNCTION bf;
    bf.BlendOp = AC_SRC_OVER;
    bf.BlendFlags = 0;
    bf.SourceConstantAlpha = 128;   // 0で透明、255で不透明
    bf.AlphaFormat = 0;             // 画素ごとのアルファは使わない

    pDC->AlphaBlend(x, y, cx, cy, &dcSrc, 0, 0, cx, cy, bf);

    dcSrc.SelectObject(pOldBitmap);
}

AlphaFormatが0のときの色は、赤・緑・青のそれぞれについて次の式で決まります(BLENDFUNCTION)。

転送先 = 転送元 × (SourceConstantAlpha / 255) + 転送先 × (1 - SourceConstantAlpha / 255)

どの画素にも同じ割合を使うので、1のようにマークの周りの白い四角も同じ濃さで重なります。ウィンドウ全体を薄く重ねる、選択した範囲を半透明の色で塗る、といった四角い範囲の用途ならこれで足り、ビットマップもCreateCompatibleBitmapで作った普通のものでかまいません。メモリDCに画像を選択して転送先のDCへ描く流れは、BitBltとStretchBltの使い分けと同じです。

AlphaBlendの本体はmsimg32.dllにあります。MFCのヘッダーafx.hに#pragma comment(lib, "msimg32.lib")が書かれているので、MFCのプロジェクトは設定を変えずにリンクできます。MFCを使わないWin32のアプリでは、リンカーの入力にMsimg32.libを足します。


AC_SRC_ALPHAで形に沿って重ねる:32ビットDIBと乗算済みアルファ

丸い形だけを重ねたり縁をぼかしたりするには、画素ごとにアルファ(不透明さ)を持ったビットマップを作り、AlphaFormatにAC_SRC_ALPHAを指定します。このときの条件は次の2つです。

  • 転送元のビットマップは32bppでなければならず、そうでないとAlphaBlendは失敗します。
  • AlphaBlendは乗算済みのアルファを使います。赤・緑・青の値に、その画素のアルファを掛けて0xffで割った値を、呼び出す前に入れておく必要があります。

画素の値を自分で書き込めるように、CreateDIBSectionで32ビットのDIBを作ります。マークの色と「!」はGDIで描き、描き終えてからアルファを入れて色に掛けます(2の作り方)。

// CMonitorDlg.h
CBitmap m_bmpBadge;     // 縁をぼかした警告マーク(乗算済みアルファの32ビットDIB)

// CMonitorDlg.cpp
static const int s_cxBadge = 72;
static const int s_cyBadge = 72;

// 中心から半径24までは不透明、半径34に向かって0まで下げる
static BYTE CalcBadgeAlpha(int x, int y)
{
    const double dx = x + 0.5 - s_cxBadge / 2.0;
    const double dy = y + 0.5 - s_cyBadge / 2.0;
    const double dRadius = sqrt(dx * dx + dy * dy);

    if (dRadius <= 24.0)
        return 255;
    if (dRadius >= 34.0)
        return 0;
    return (BYTE)(255.0 * (34.0 - dRadius) / 10.0 + 0.5);
}

BOOL CMonitorDlg::CreateBadge()
{
    // 1. 32ビットのDIBを作る(biHeightを負にすると上の行から並ぶ)
    BITMAPINFO bmi;
    ZeroMemory(&bmi, sizeof(bmi));
    bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
    bmi.bmiHeader.biWidth = s_cxBadge;
    bmi.bmiHeader.biHeight = -s_cyBadge;
    bmi.bmiHeader.biPlanes = 1;
    bmi.bmiHeader.biBitCount = 32;
    bmi.bmiHeader.biCompression = BI_RGB;

    void* pBits = NULL;
    HBITMAP hBitmap = ::CreateDIBSection(NULL, &bmi, DIB_RGB_COLORS, &pBits, NULL, 0);
    if (hBitmap == NULL)
        return FALSE;
    m_bmpBadge.Attach(hBitmap);

    // 2. GDIで色と「!」を描く
    CClientDC dc(this);
    CDC dcBadge;
    dcBadge.CreateCompatibleDC(&dc);
    CBitmap* pOldBitmap = dcBadge.SelectObject(&m_bmpBadge);

    dcBadge.FillSolidRect(0, 0, s_cxBadge, s_cyBadge, RGB(250, 190, 20));

    CFont font;
    font.CreateFont(-44, 0, 0, 0, FW_BOLD, FALSE, FALSE, FALSE, DEFAULT_CHARSET,
        OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH, _T("Segoe UI"));
    CFont* pOldFont = dcBadge.SelectObject(&font);
    dcBadge.SetBkMode(TRANSPARENT);
    dcBadge.SetTextColor(RGB(0, 0, 0));
    CRect rect(0, 0, s_cxBadge, s_cyBadge);
    dcBadge.DrawText(_T("!"), rect, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
    dcBadge.SelectObject(pOldFont);

    dcBadge.SelectObject(pOldBitmap);

    // 3. GDIの描画を終わらせてから、画素ごとにアルファを入れて色に掛ける
    GdiFlush();

    DWORD* pdwBits = (DWORD*)pBits;
    for (int y = 0; y < s_cyBadge; y++)
    {
        for (int x = 0; x < s_cxBadge; x++)
        {
            DWORD& dwPixel = pdwBits[y * s_cxBadge + x];
            const DWORD dwAlpha = CalcBadgeAlpha(x, y);
            const DWORD dwRed = ((dwPixel >> 16) & 0xFF) * dwAlpha / 255;
            const DWORD dwGreen = ((dwPixel >> 8) & 0xFF) * dwAlpha / 255;
            const DWORD dwBlue = (dwPixel & 0xFF) * dwAlpha / 255;
            dwPixel = (dwAlpha << 24) | (dwRed << 16) | (dwGreen << 8) | dwBlue;
        }
    }
    return TRUE;
}

void CMonitorDlg::DrawBadge(CDC* pDC, int x, int y)
{
    CDC dcSrc;
    dcSrc.CreateCompatibleDC(pDC);
    CBitmap* pOldBitmap = dcSrc.SelectObject(&m_bmpBadge);

    BLENDFUNCTION bf;
    bf.BlendOp = AC_SRC_OVER;
    bf.BlendFlags = 0;
    bf.SourceConstantAlpha = 255;   // 画素ごとのアルファだけを使う
    bf.AlphaFormat = AC_SRC_ALPHA;

    pDC->AlphaBlend(x, y, s_cxBadge, s_cyBadge, &dcSrc, 0, 0, s_cxBadge, s_cyBadge, bf);

    dcSrc.SelectObject(pOldBitmap);
}

CreateBadgeはOnInitDialogで1回呼び、DrawBadgeはOnPaintから呼びます(sqrtを使うので<math.h>をインクルードします)。32ビットのDIBの1画素はDWORD 1つで、上の桁からアルファ・赤・緑・青の順に入っています。画素を自分で読み書きする前には、GdiFlushを呼んでGDIの描画を終わらせておきます(CreateDIBSection)。

色にアルファを掛けないと縁に別の色が出る

AC_SRC_ALPHAを指定してSourceConstantAlphaが255のときの式は次のとおりで、転送元の色にはもうアルファが掛かっている前提です。アルファは0〜255の値なので、255で割って0〜1にしています。

転送先 = 転送元 + (1 - 転送元のアルファ / 255) × 転送先

3は、手順3で色に掛けずにアルファだけを入れたものです。縁のアルファが小さい所でも黄色の値(緑なら190)がそのまま足されるので、計算の結果が255を超えます。たとえば明るい背景の緑241の上では、アルファが半分の画素で190+241×0.5=310になります。画面では明るい背景の上に紫、暗い背景の上に緑の輪が出ました。255を超えた値の扱いは公式の説明に無く、出た色からは超えた分が0の側へ回り込んだように見えます。

PNGなどから読み込んだ画像も、色にアルファが掛かっていないことがあります。その場合も手順3と同じように掛けてから渡します。

形に沿ったまま全体も薄くする

AC_SRC_ALPHAとSourceConstantAlphaは一緒に使えます。4は2と同じビットマップをSourceConstantAlpha 128で描いたもので、円の形とぼけた縁はそのままで全体が薄くなります。このとき転送元の色とアルファの両方にSourceConstantAlpha / 255を掛けてから、上の式で重ねます。点滅や、ゆっくり消えていく表示に使えます。


GDIのDrawTextで描いた所はアルファが0になった

5は手順2と3の順番を入れ替え、アルファを入れたあとでDrawTextで「!」を描いたもので、「!」の所が抜けて後ろの格子が見えます。アルファを255にしておいた円の内側でも、「!」を描いた画素のアルファは0になっていました。

アルファが0で色が黒の画素は、上の式では転送先がそのまま残るので、背景が見えます。32ビットのDIBにDrawTextのようなGDIの関数で描くと描いた画素のアルファが0になることがあるので、文字や線はアルファを入れる前に描きます(手順2・3の順番)。

6は、1の普通のビットマップにAC_SRC_ALPHAを付けて描いたものです。何も描かれず、戻り値はTRUEでした。画面に合わせて32ビットで作られたビットマップですが、GetDIBitsで読み出すとアルファの位置(上位8ビット)はすべて0でした。画面の色数が32ビットでなければビットマップも32ビットにならないので、上の条件のとおりAlphaBlendが失敗します。AC_SRC_ALPHAは、2のようにアルファを自分で入れたビットマップのときだけ付けます。


AlphaBlendが失敗するときと、拡大縮小

AlphaBlendには次の決まりもあります。

  • 幅や高さに負の値を渡すと失敗します(StretchBltのような反転はできません)。
  • 転送元の四角形がビットマップからはみ出していると失敗します。
  • 転送元と転送先が同じ画面か同じビットマップで、四角形が重なると失敗します。
  • 大きさが違うと転送元を引き伸ばして描きます。SetStretchBltModeでHALFTONEなどを設定しても、COLORONCOLORになります。
  • プリンターでは、GetDeviceCapsのSHADEBLENDCAPSで対応しているかを先に調べます。

まとめ

  • 画像全体を同じ濃さで薄くするなら、普通のビットマップをSourceConstantAlphaだけで描きます。
  • 形に沿って重ねるなら、CreateDIBSectionで32ビットのDIBを作り、色にアルファを掛けてAC_SRC_ALPHAで描きます。
  • GDIで描くのはアルファを入れる前にし、GdiFlushを呼んでから画素を書き換えます。
目次