iooiau.net

Vistaの透け透けウィンドウにアルファ付き画像を描画する

Windows Vista/7 では、以下のようにウィンドウのクライアント領域の一部を透けさせることができます。
上半分を透けさせたウィンドウ
そこに、以下のようなアルファ付きの画像を描画する方法を紹介します。
アルファ付きのキャラクタ

まず、クライアント領域の一部を透けさせるための処理を書きます。
ウィンドウを透けさせるには、Windows Vista 以降で利用できる DWM (Desktop Window Manager) の機能を使います。


#include <windows.h>  // いつものヘッダ
#include <dwmapi.h>   // DWM のヘッダ

// DWM のライブラリをリンクする指定です
#pragma comment(lib, "dwmapi.lib")

// ウィンドウを透けさせる関数です
bool ApplyAero(
    HWND hwnd,               // 透けさせるウィンドウのハンドルです
    const MARGINS *pMargins  // 透けさせる範囲です
)
{
    // まず透け透け機能(?)が利用できるか調べます
    BOOL fEnabled;
    if (DwmIsCompositionEnabled(&fEnabled) != S_OK || !fEnabled)
        return false;  // 透け透け機能が利用できない

    // ウィンドウの一部を透けさせます
    return DwmExtendFrameIntoClientArea(hwnd, pMargins) == S_OK;
}

ここで出てくる MARGINS 構造体は、透けさせる範囲を指定するために使用します。
この構造体は以下のように定義されています。


typedef struct _MARGINS {
    int cxLeftWidth;     // 左側の範囲です
    int cxRightWidth;    // 右側の範囲です
    int cyTopHeight;     // 上側の範囲です
    int cyBottomHeight;  // 下側の範囲です
} MARGINS, *PMARGINS;

例えば以下のように呼び出せば、クライアント領域の左側10ピクセルと下側30ピクセルが透けます。


MARGINS margins = {10, 0, 0, 30};
ApplyAero(hwnd, &margins);

また、範囲のいずれかに -1 を設定すると、クライアント領域全体が透けて表示されます。

次に、アルファ付きの画像を読み込んでウィンドウに描画する処理を書きます。
アルファ付きの画像は、通常ビットマップの描画に使用する BitBlt や StretchBlt を使えません。
一応 AlphaBlend (GdiAlphaBlend) というものもありますが、アルファ値を Premultiply という形式にして渡す必要があるため、扱いづらいです。
ですので、ここでは GDI+ の関数を使います。
GDI+ の機能を利用する場合、最初に初期化処理を行い、最後に終了処理を行う必要があります。
これは定型的な処理ですので、以下のように書いてプログラムの開始時と終了時に呼べばOKです。


#include <gdiplus.h>  // GDI+ のヘッダ

// GDI+ のライブラリをリンクする指定です
#pragma comment(lib, "gdiplus.lib")

bool fInitialized = false;
ULONG_PTR Token;

// GDI+ の初期化を行う関数です
bool InitializeGdiPlus()
{
    Gdiplus::GdiplusStartupInput si;
    if (Gdiplus::GdiplusStartup(&Token, &si, NULL) != Gdiplus::Ok)
        return false;
    fInitialized = true;
    return true;
}

// GDI+ の終了処理を行う関数です
void FinalizeGdiPlus()
{
    if (fInitialized)
        Gdiplus::GdiplusShutdown(Token);
}

次に、描画する画像を読み込む処理を書きます。
アルファ付きの画像には PNG 形式を利用すると、対応ソフトも多くサイズも小さくできるのでいいですね。
ファイルから画像を読み込むのは、以下のように簡単に行えます。


Gdiplus::Bitmap *pBitmap = Gdiplus::Bitmap::FromFile(L"Sample.png");

これだけで、BMP/GIF/JPEG/PNG/TIFF 形式の画像を読み込むことができます。
しかし、画像をファイルではなくリソースに格納しておきたい場合もありますね。 そういう場合のために FromResource という関数もありますが、これは BMP 形式しか読み込むことができません。
PNG などの形式をリソースから読み込みたい場合は、ちょっとしたコードを書く必要があります。


// リソースから画像を読み込む関数です
Gdiplus::Bitmap *LoadImageFromResource(
    HINSTANCE hinst,  // 読み込み元のインスタンスのハンドルです
    LPCTSTR pszName,  // 読み込むリソースの名前です
    LPCTSTR pszType   // 読み込むリソースの種類です
)
{
    // 指定されたリソースを探します
    HRSRC hRes = FindResource(hinst, pszName, pszType);
    if (hRes == NULL)
        return NULL;

    // リソースのサイズを取得します
    DWORD Size = SizeofResource(hinst, hRes);
    if (Size == 0)
        return NULL;

    // リソースを読み込みます
    HGLOBAL hData = LoadResource(hinst, hRes);
    if (hData == NULL)
        return NULL;
    const void *pData = LockResource(hData);
    if (pData == NULL)
        return NULL;

    // リソースのデータをコピーします
    HGLOBAL hBuffer = GlobalAlloc(GMEM_MOVEABLE, Size);
    if (hBuffer == NULL)
        return NULL;
    void *pBuffer = GlobalLock(hBuffer);
    if (pBuffer == NULL) {
        GlobalFree(hBuffer);
        return NULL;
    }
    CopyMemory(pBuffer, pData, Size);
    GlobalUnlock(hBuffer);

    // 画像を読み込みます
    IStream *pStream;
    if (CreateStreamOnHGlobal(hBuffer, TRUE, &pStream) != S_OK) {
        GlobalFree(hBuffer);
        return NULL;
    }
    Gdiplus::Bitmap *pBitmap = Gdiplus::Bitmap::FromStream(pStream);
    pStream->Release();

    return pBitmap;
}

以下のようにリソーススクリプト(*.rc)に記述して、画像ファイルをリソースに埋め込みます。


Image1 IMAGE "Sample.png"

この場合以下のようにすれば、画像をリソースから読み込むことができます。


Gdiplus::Bitmap *pBitmap = LoadImageFromResource(hInstance, TEXT("Image1"), TEXT("IMAGE"));

さて、それではいよいよ描画処理を記述します。
といっても基本は簡単です。


// デバイスコンテキストのハンドルから Graphics オブジェクトを作成します
Gdiplus::Graphics graphics(hdc);

// 背景を透明色でクリアします
graphics.Clear(Gdiplus::Color(0,0,0,0));

// 画像を描画します
graphics.DrawImage(pBitmap, x, y, pBitmap->GetWidth(), pBitmap->GetHeight());

Graphics というのは、GDI+ 版のデバイスコンテキストのようなものです。 GDI+ ではこのクラスを使って描画処理を行います。
コンストラクタにデバイスコンテキストのハンドルを指定すれば、そこに描画することができます。
ここでは背景を透明にするために、透明色でクリアしてから画像を描画しています。

以上の処理を組み合わせれば、透け透けのウィンドウに画像を描画することができますね。
それでは実際にウィンドウを作成して試してみましょう。
この例ではウィンドウのクライアント領域の上半分を透けさせて、透けている部分と透けていない部分にまたがるように画像を描画しています。


#include <windows.h>  // いつものヘッダ
#include <tchar.h>    // ANSI と UNICODE 両対応のためのヘッダ

// ウィンドウクラス名です
#define MAIN_WINDOW_CLASS TEXT("Main Window Class")

// インスタンスハンドルです
HINSTANCE hInst;

// クライアント領域の上半分を透けさせます
void SetWindowGlass(HWND hwnd)
{
     RECT rc;
     MARGINS margins = {0, 0, 0, 0};
     GetClientRect(hwnd, &rc);
     margins.cyTopHeight = rc.bottom / 2;
     ApplyAero(hwnd, &margins);
}

// 描画を行う関数です
void Draw(
    HWND hwnd,                // 描画先のウィンドウのハンドルです
    HDC hdc,                  // 描画先のデバイスコンテキストのハンドルです
    Gdiplus::Bitmap *pBitmap  // 描画する画像です
)
{
    // デバイスコンテキストのハンドルから Graphics オブジェクトを作成します
    Gdiplus::Graphics graphics(hdc);

    // 背景を透明色でクリアします
    graphics.Clear(Gdiplus::Color(0,0,0,0));

    // クライアント領域の矩形を取得します
    RECT rc;
    GetClientRect(hwnd, &rc);

    // 下半分を白で塗りつぶします
    Gdiplus::SolidBrush brush(Gdiplus::Color(255, 255, 255, 255));
    graphics.FillRectangle(&brush, 0, rc.bottom / 2,
                           rc.right, rc.bottom - (rc.bottom / 2));

    // 中心に画像を描画します
    int x, y;
    x = (rc.right - pBitmap->GetWidth()) / 2;
    y = (rc.bottom - pBitmap->GetHeight()) / 2;
    graphics.DrawImage(pBitmap, x, y, pBitmap->GetWidth(), pBitmap->GetHeight());
}

// ウィンドウプロシージャです
LRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    static Gdiplus::Bitmap *pBitmap = NULL;

    switch (uMsg) {
    case WM_CREATE:
        {
            // クライアント領域を透けさせます
            SetWindowGlass(hwnd);

            // 画像を読み込みます
            pBitmap = LoadImageFromResource(hInst, TEXT("Image1"), TEXT("IMAGE"));
            if (pBitmap == NULL)
                return -1;  // 画像の読み込み失敗
        }
        return 0;

    case WM_PAINT:
        {
            PAINTSTRUCT ps;

            // 描画を行います
            BeginPaint(hwnd, &ps);
            Draw(hwnd, ps.hdc, pBitmap);
            EndPaint(hwnd, &ps);
        }
        return 0;

    case WM_DESTROY:
        // 画像を破棄します
        delete pBitmap;

        // プログラムを終了させます
        PostQuitMessage(0);
        return 0;
    }
    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

// エントリポイントです
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                       LPTSTR pszCmdLine, int nCmdShow)
{
    // インスタンスハンドルを保持します
    hInst = hInstance;

    // ウィンドウクラスを登録します
    WNDCLASS wc;
    wc.style         = 0;
    wc.lpfnWndProc   = WndProc;
    wc.cbClsExtra    = 0;
    wc.cbWndExtra    = 0;
    wc.hInstance     = hInst;
    wc.hIcon         = NULL;
    wc.hCursor       = LoadCursor(NULL,IDC_ARROW);
    wc.hbrBackground = NULL;
    wc.lpszMenuName  = NULL;
    wc.lpszClassName = MAIN_WINDOW_CLASS;
    RegisterClass(&wc);

    // GDI+ の初期化です
    if (!InitializeGdiPlus()) {
        MessageBox(NULL, TEXT("GDI+ の初期化ができません。"), NULL, MB_OK | MB_ICONSTOP);
        return 0;
    }

    // ウィンドウを作成します
    HWND hwnd = CreateWindowEx(0, MAIN_WINDOW_CLASS, TEXT("Sample"),
                               WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX,
                               CW_USEDEFAULT, CW_USEDEFAULT, 256, 256,
                               NULL, NULL, hInst, NULL);
    if (hwnd == NULL) {
        MessageBox(NULL, TEXT("ウィンドウが作成できません。"), NULL, MB_OK | MB_ICONSTOP);
        FinalizeGdiPlus();
        return 0;
    }
    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    // メッセージループです
    MSG msg;
    BOOL Result;
    while ((Result = GetMessage(&msg, NULL, 0, 0)) != 0 && Result != -1) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    // GDI+ の終了処理です
    FinalizeGdiPlus();

    return msg.wParam;
}

このプログラムを実行すると、以下のようなウィンドウが表示されます。
実行結果
ウィンドウの上半分だけが透けて表示され、その上にキャラクターが描画されています。
実用性はほとんどありませんが、なかなか面白い演出ができますね。

なお、前のコードには少し問題があります。
それはこのプログラムを実行している途中に Aero の有効/無効が切り替わった場合、それに追従できないことです。
例えば実行中に Aero が無効になって再び有効になった場合、ウィンドウが透けなくなってしまいます。
この問題に対処するためには、Aero の有効/無効が切り替わった時に送られてくるメッセージ WM_DWMCOMPOSITIONCHANGED で、WM_CREATE と同様にウィンドウを透けさせる関数を呼び出します。


case WM_DWMCOMPOSITIONCHANGED:
    // Aero の有効/無効が切り替わったので再設定します
    SetWindowGlass(hwnd);
    return 0;

サンプルのソースコードと実行ファイルを書庫にまとめました。Aero.zip (106,429 バイト)
サンプルの画像には「キャラクターなんとか機」で作成した画像を使用させて頂いています。