Programming Field - プログラミング Tips

Vista以降のテーマを用いた「アニメーション」描画のやり方

Vista以降の標準コントロールでは、テーマ(一部を除く)を有効にしているとマウスカーソルを合わせるとじわりとハイライトし、カーソルを別の場所に移動するとフェードアウトするように元の状態に戻るような描画がなされます(クロスフェード)。

アニメーションを気にしない場合は、DrawThemeBackground関数(英語)などを使うだけでテーマを用いた描画が可能なのですが、他のコントロールに合わせてアニメーションを入れたい場合は、BeginBufferedAnimation関数(英語)やそれに関係する関数を使って描画を行う必要があります。

ただし描画側でアニメーションのためのタイマーを自力で作成する必要は無く、アニメーション前と後の(絵の)状態を描画してあげるだけでAPI側が自動的にアニメーションを行ってくれるため、わりと簡単にアニメーションを実現することが出来ます。

以下はWM_PAINT時にボタンの描画(と仮定)を行う際にアニメーション処理を加えたコードの例です。(具体的なボタンの描画は「DrawMyButton」というユーザー定義の描画関数が行うとします。)

HWND hWnd;          // ウィンドウのハンドル
BOOL bPushed;       // ボタンが押されているかどうか
BOOL bDisabled;     // ボタンが無効になっているかどうか
BOOL bHover;        // マウスポインタがボタン上にあるかどうか
BOOL bOldPushed;    // bPushedが変更される直前の値
BOOL bOldDisabled;  // bDisabledが変更される直前の値
BOOL bOldHover;     // bHoverが変更される直前の値

    HDC hDC;
    PAINTSTRUCT ps;
    hDC = BeginPaint(hWnd, &ps);
    // アニメーション実行中のタイマーから呼び出されたかどうか調べる
    // (タイマー内から呼び出された場合は自動的に描画が行われる)
    if (!BufferedPaintRenderAnimation(hWnd, hDC))
    {
        // ボタンの領域を取得する
        RECT rcClient;
        GetClientRect(hWnd, &rcClient);

        // 古い状態と新しい状態からフラグを設定する
        int iFromState, iToState;
        if (bOldPushed)
            iFromState = PBS_PRESSED;
        else if (bOldDisabled)
            iFromState = PBS_DISABLED;
        else
            iFromState = (bOldHover ? PBS_HOT : PBS_NORMAL);
        if (bPushed)
            iToState = PBS_PRESSED;
        else if (bDisabled)
            iToState = PBS_DISABLED;
        else
            iToState = (bHover ? PBS_HOT : PBS_NORMAL);
        // 状態が変化しているか調べる
        if (iFromState != iToState)
        {
            // 2012/02/06更新 - この呼び出しは不要でした。
            //// 実行中のアニメーションを止める(実行中の場合止めないと予期しない表示に…)
            //BufferedPaintStopAllAnimations(hWnd);
            // ボタンのテーマオブジェクトを取得する
            HTHEME hTheme = OpenThemeData(hWnd, L"Button");

            BP_ANIMATIONPARAMS animParams = { sizeof(BP_ANIMATIONPARAMS) };
            animParams.style = BPAS_LINEAR; // 現在はこのフラグのみ使用可能
            if (hTheme)
            {
                // フェードする時間を取得する
                GetThemeTransitionDuration(hTheme, BP_PUSHBUTTON, iFromState, iToState,
                    TMT_TRANSITIONDURATIONS, &animParams.dwDuration);
                CloseThemeData(hTheme);
            }
            else
            {
                // テーマが取得できなかったのでフェードする時間を適当に設定する
                // (0に設定するとフェードせずにすぐ切り替えることが出来る)
                animParams.dwDuration = (bHover) ? 200 : 800;
            }

            // アニメーションバッファを作成する
            HDC hDCFrom = NULL, hDCTo = NULL;
            HANIMATIONBUFFER hab = BeginBufferedAnimation(hWnd, hDC, &rcClient,
                BPBF_COMPATIBLEBITMAP, NULL, &animParams, &hDCFrom, &hDCTo);
            if (hab)
            {
                // 作成に成功した場合、hDCFromとhDCToにそれぞれ
                // 変更前後のグラフィックを描画する
                // (hDCFromやhDCToがNULLになる場合もある)
                if (hDCFrom)
                    DrawMyButton(hDCFrom, &rcClient, iFromState);
                if (hDCTo)
                    DrawMyButton(hDCTo, &rcClient, iToState);

                // 描画が終了したらバッファを解放してアニメーションを開始する
                EndBufferedAnimation(hab, TRUE);
            }
            else
            {
                // バッファが作成できなかった場合は普通の方法で描画を行う
                DrawMyButton(hDC, &rcClient, iToState);
            }

            // 古いフラグを更新する
            bOldPushed = bPushed;
            bOldDisabled = bDisabled;
            bOldHover = bHover;
        }
        else
        {
            // 状態変化がない場合は普通の方法で描画を行う
            DrawMyButton(hDC, &rcClient, iToState);
        }
    }
    EndPaint(hWnd, &ps);

BeginBufferedPaint関数という(BeginBufferedAnimationと引数が若干似ている)描画時に便利な関数がありますが、これとBeginBufferedAnimation関数を同時に利用すると上手くアニメーションを行えない場合があります。詳しい追跡調査が出来ていませんが、BeginBufferedPaint関数を使う場合は注意する必要があります。

参考:

最終更新日: 2011/06/02