【MFC】TransparentBltで背景色を抜いて描く:透過色の指定と、縮小した画像の縁に色が残る所

【MFC】TransparentBltで背景色を抜いて描く:透過色の指定と、縮小した画像の縁に色が残る所

背景をマゼンタで塗ったアイコンのビットマップをBitBltで重ねると、マゼンタの四角までそのまま描かれます。TransparentBltを使うと、透過色に指定した色の画素は転送されないので、アイコンの形だけが下の絵の上に描かれます。MFCではCDC::TransparentBltを呼べばよく、msimg32.libをプロジェクトに足す必要もありません。ただし、先に縮小しておいた画像を渡すと、縁に透過色に近い色の画素が残ることがあります。


目次

BitBltとTransparentBltで描いた結果

配置図(格子と青い帯)の上に、背景をRGB(255, 0, 255)で塗った32×40ピクセルのマーカーを6通りの方法で描いた結果です。下の段の右の枠は、マーカーの左上の小さな枠の中を5倍に拡大したものです。

BitBltではマゼンタの四角ごと描かれ、TransparentBltとマスクでは背景色が抜け、HALFTONEで縮小しておいた画像では縁にマゼンタの画素が残っている画面
1はマゼンタの四角ごと写る。2と3は抜ける。5だけ縁にマゼンタとピンクの画素が残る
番号描き方背景色縁
1BitBlt(SRCCOPY)四角ごと写る-
2TransparentBlt(同じ大きさ)抜けるきれい
3マスクを作ってSRCANDとSRCPAINTの2回のBitBlt抜けるきれい
4TransparentBltで2倍に拡大抜ける階段状になる
5StretchBltのHALFTONEで縮小しておいた画像をTransparentBltほとんど抜けるマゼンタとピンクの画素が残る
6大きい画像をTransparentBltで直接縮小抜ける色は残らないが階段状

TransparentBltで透過色を指定して描く

BitBltと同じように、画像を選択したメモリDCを作り、そこから画面のDCへ転送します。BitBltと違って、転送元の幅と高さを別に渡し、最後の引数に透過色を渡します。

// CMapDlg.h
CBitmap m_bmpMarker;    // 背景をRGB(255, 0, 255)で塗ったマーカーの画像

// CMapDlg.cpp
void CMapDlg::DrawMarker(CDC* pDC, int x, int y)
{
    CDC dcSrc;
    dcSrc.CreateCompatibleDC(pDC);
    CBitmap* pOldBitmap = dcSrc.SelectObject(&m_bmpMarker);

    BITMAP bm;
    m_bmpMarker.GetBitmap(&bm);

    pDC->TransparentBlt(x, y, bm.bmWidth, bm.bmHeight,
        &dcSrc, 0, 0, bm.bmWidth, bm.bmHeight, RGB(255, 0, 255));

    dcSrc.SelectObject(pOldBitmap);
}

引数は前から、転送先の左上・幅・高さ、転送元のDC、転送元の左上・幅・高さ、透過色の順です(TransparentBlt)。透過色と同じ値の画素だけが抜け、上の画面の5の縁に残ったピンクの画素のように、マゼンタに近い色でも値が違えばそのまま描かれます。画像の背景は、透過色と同じ値で塗ります。

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

ほかに、次の点に気をつけます。

  • 互換ビットマップ(DDB)で使う関数です。
  • 32ビットのビットマップでも、アルファの値はそのまま写すだけです。半透明に重ねたいときは、TransparentBltではなくAlphaBlendを使います。
  • 幅や高さを負の値にしても、StretchBltのような左右・上下の反転はしません。
  • マルチモニターの環境では、転送元と転送先のDCが同じデバイスのものでないと失敗します。

マスクとSRCAND・SRCPAINTで抜く昔のやり方

古いアプリには、白黒のマスクを作ってBitBltを2回呼び、背景色を抜くコードもよく残っています。上の画面の3がこの方法で、TransparentBltで描いた2と同じ見た目になります。

void CMapDlg::DrawWithMask(CDC* pDC, int x, int y, CDC* pSrcDC, int cx, int cy, COLORREF crTransparent)
{
    // 1. 透過色の画素が白、それ以外が黒のマスクを作る
    CDC dcMask;
    dcMask.CreateCompatibleDC(pDC);
    CBitmap bmpMask;
    bmpMask.CreateBitmap(cx, cy, 1, 1, NULL);
    CBitmap* pOldMask = dcMask.SelectObject(&bmpMask);

    const COLORREF crOldSrcBk = pSrcDC->SetBkColor(crTransparent);
    dcMask.BitBlt(0, 0, cx, cy, pSrcDC, 0, 0, SRCCOPY);
    pSrcDC->SetBkColor(crOldSrcBk);

    // 2. 画像の写しを作り、透過色の所を黒にする
    CDC dcImage;
    dcImage.CreateCompatibleDC(pDC);
    CBitmap bmpImage;
    bmpImage.CreateCompatibleBitmap(pDC, cx, cy);
    CBitmap* pOldImage = dcImage.SelectObject(&bmpImage);

    dcImage.BitBlt(0, 0, cx, cy, pSrcDC, 0, 0, SRCCOPY);
    dcImage.SetBkColor(RGB(0, 0, 0));
    dcImage.SetTextColor(RGB(255, 255, 255));
    dcImage.BitBlt(0, 0, cx, cy, &dcMask, 0, 0, SRCAND);

    // 3. 転送先の絵の形の所だけを黒く抜き、そこへ画像をORで重ねる
    const COLORREF crOldBk = pDC->SetBkColor(RGB(255, 255, 255));
    const COLORREF crOldText = pDC->SetTextColor(RGB(0, 0, 0));
    pDC->BitBlt(x, y, cx, cy, &dcMask, 0, 0, SRCAND);
    pDC->BitBlt(x, y, cx, cy, &dcImage, 0, 0, SRCPAINT);
    pDC->SetTextColor(crOldText);
    pDC->SetBkColor(crOldBk);

    dcImage.SelectObject(pOldImage);
    dcMask.SelectObject(pOldMask);
}

このコードは、カラーとモノクロのビットマップのあいだで転送するときの変換を使っています(StretchBlt)。カラーからモノクロへ変換するときは、背景色(SetBkColorの色)と同じ画素が白、それ以外が黒になります。モノクロからカラーへ変換するときは、白が背景色に、黒が文字色(SetTextColorの色)になります。

  • 1では、転送元の背景色を透過色にしてからモノクロのビットマップへ写すので、透過色の所が白、マーカーの所が黒のマスクになります。
  • 2では、背景色を黒、文字色を白にしてマスクをSRCAND(AND)で重ねます。マスクの白は黒に、黒は白に変わるので、画像の透過色の所だけが黒になり、マーカーの色は残ります。
  • 3では、転送先の背景色を白、文字色を黒にしてマスクをSRCANDで重ねます。透過色の所(白)は下の絵がそのまま残り、マーカーの形の所だけが黒になります。そこへ2の画像をSRCPAINT(OR)で重ねると、黒の所にマーカーの色が入り、透過色だった所は黒とのORなので下の絵のままです。

背景色と文字色の設定を1つでも間違えると、黒い四角が出たり色が反転したりします。新しく書くならTransparentBltの1行で足ります。すでに動いているコードは、マスクの方法のままでもかまいません。


拡大縮小と組み合わせたときに縁に透過色が残る

TransparentBltは、転送元と転送先の大きさが違うと、転送元を引き伸ばしたり縮めたりして描きます。

// 128×160で作ったマーカーの画像を、64×80に縮めて描く
pDC->TransparentBlt(x, y, 64, 80, &dcSrc, 0, 0, 128, 160, RGB(255, 0, 255));

上の画面の4(32×40を64×80に拡大)と6(128×160を64×80に縮小)は、どちらも背景色が抜けています。TransparentBltは、伸縮モードがBLACKONWHITE(既定)かWHITEONBLACKのDCではCOLORONCOLORとして扱い、隣の画素と色を混ぜずに描くので、縁は階段状になります。

5は、同じ128×160の画像を先にStretchBltのHALFTONEで64×80に縮めておき、その画像を等倍のTransparentBltで描いたものです。HALFTONEで縮めると、縁の画素はマーカーの赤と背景のマゼンタが混ざった色になります。この色は透過色のRGB(255, 0, 255)とは値が違うので抜けず、円の縁に沿ってピンクやマゼンタに近い画素が残ります。BitBltとStretchBltの使い分けでは縮小の画質を上げるためにHALFTONEを勧めていますが、透過色を使う画像を先にHALFTONEで縮めるとこうなります。

縁に色を残さないようにするには、次のどれかにします。

  • 縮小は先にしないで、元の画像をTransparentBltに渡して縮めます(上の画面の6)。縁は階段状になります。
  • 表示する大きさの画像を、縁を透過色と混ぜずに用意しておき、等倍で描きます(上の画面の2)。
  • 縁をなめらかに見せたいときは、透過色ではなくアルファ値を持った32ビットの画像にして、AlphaBlendで描きます。

縮小していない画像でも、背景をマゼンタにしたままペイントソフトでなめらかな縁で描いたものは、縁の画素が透過色と混ざった色になっているので、同じように縁が残ります。


まとめ

  • CDC::TransparentBltの最後の引数に透過色を渡すと、その色と同じ値の画素だけが描かれません。MFCのアプリではmsimg32.libを足さなくてもリンクできます。
  • マスクとSRCAND・SRCPAINTの2回のBitBltでも同じ見た目になりますが、背景色と文字色の設定が要ります。
  • HALFTONEで縮小しておいた画像は縁に色が残ります。縮小はTransparentBltに任せるか、縁をなめらかにしたいならAlphaBlendを使います。
目次