◆レイヤードウィンドウ - VC++ 2005 MFC (スケスケ、アニメーションなウィンドウ) その1

[ ホーム ] [ 上へ ] [ レイヤードウィンドウ - VC++ MFC その2 ]

実務にPCを使っていると、フェードアウトするウィンドウや、スケスケな ウィンドウはほとんど縁がないし、逆に邪魔な場合が多いです。でも、最近は見た目も凝ったものが 多くなり、まったく無視できなくなり素人なりにちょっと調べてみました。

・概要

Windowのある特定の色だけ透過したり、全体(一部)を 透明(半透明)化したりしてアニメーションが実現できますが、基本になる関数は

  1. CWnd::AnimateWindow(DWORD dwTime, DWORD dwFlags);
    
  2. CWnd::SetLayeredWindowAttributes(COLORREF crKey, 
                                     BYTE bAlpha, 
                                     DWORD dwFlags);
    
  3. CWnd::UpdateLayeredWindow(CDC   *pDCDst, POINT *pptDst, SIZE *psize, 
                              CDC *pDCSrc,   POINT *pptSrc, COLORREF crKey, 
                              BLENDFUNCTION *pblend, DWORD dwFlags);
    

で、上記のメンバ関数名と同名のWin32APIをラップしています。
このうち、1、2は簡単に使えますが、1.は定型のアニメーションしかできませんし、 2.はWindow全体に対してにしか透過や透明化ができません。3.は引数の数から してややこしそうですが、実際自分にはほんと難しいです。しかし、Windowの特定の一部分だけを 透明化できたりと細かいことができるので、VC++にはうってつけ?です。

今回はそれぞれの関数でフェードアウトで徐々にウィンドウが 消えていきながら閉じる機能を実装してみます。

・準備

早速プロジェクトを作成します。Visual Studioのメニューの ファイル(F)→新規作成(N)→プロジェクト(P)で以下のプロジェクトを作成してください。

<新しいプロジェクトダイアログボックス>

プロジェクトの種類(P) ・・・Visual C++
テンプレート・・・・・・・・MFCアプリケーション
プロジェクト名・・・・・・・任意(以下ではtestLayeredWindowとしています)

MFCアプリケーションウィザードで以下のようにしてください。

<アプリケーションの種類>
アプリケーションの種類・・・・・・・・・・・・シングルドキュメント
ドキュメントビューアーキテクチャサポート・・・チェックをはずす
ユニコードライブラリを使用する・・・・・・・・チェックをする
プロジェクト形式・・・・・・・・・・・・・・・MFC標準
MFCの使用法 ・・・・・・・・・・・・・・・・・共有DLLでMFCを使用

<データベースのサポート>
なし

<ユーザーインターフェイス機能>
サイズ変更、最小化ボタン最大化ボタン、システムメニューにチェック

<高度な機能>
デフォルトのまま。

<生成されたクラス>
CxxxApp(xxxはプロジェクト名)、CMainFrame、CChildViewの3つのクラスが生成されていることを
確認して完了してください。

とりあえずスケルトンができているのでビルドしてウィンドウを表示させてみてください。

<クライアント領域の背景色変更>
クライアントエリアの背景色はデフォルトで白色だと思いますが、透明化したときに見づらいので、 アクティブ時のキャプションバーの色にしておきます。いや、単に確認しやすくするためだけなので スルーしてもOKです。

クライアント領域はCViewの派生クラスが担当していて、 背景色はそのウィンドウクラス登録時に設定されるので、ChildView.cppの該当箇所
BOOL CChildView::PreCreateWindow(CREATESTRUCT& cs)を修正します。

BOOL CChildView::PreCreateWindow(CREATESTRUCT& cs)
{
    //   ・
    //   ・
    cs.lpszClass = AfxRegisterWndClass(CS_HREDRAW|CS_VREDRAW|CS_DBLCLKS,
                        ::LoadCursor(NULL, IDC_ARROW), 
                reinterpret_cast<HBRUSH>(COLOR_ACTIVECAPTION/*COLOR_WINDOW+1*/), NULL);
    return TRUE;
}

上記のように、AfxRegisterWndClassの 第三引数のブラシのハンドルをCOLOR_WINDOW+1からCOLOR_ACTIVECAPTIONに変更してください。

これ以後はCMainFrameクラスにすべてコーディングしていきます。

<メッセージハンドラ作成>
ウィンドウClose時にフェードアウト処理をしたいのでWM_CLOSEメッセージと、フェードアウト用の タイマWM_TIMERメッセージ対応するメッセージハンドラをつくります。
クラスビューでCMainFrameを選択してF4を押下してプロパティウィンドウを表示させてください。 メッセージアイコンを選択するとメッセージ一覧が表示されるので、WM_CLOSEとWM_TIMERの行で それぞれOnCloseとOnTimerを追加してください。

<メンバ変数作成>
ウィンドウの透明度(α値)を表すBYTE型のprotectedなメンバ変数m_alphaを追加します。 メンバ変数の追加ウィザードを使ってもいいですが、初期値を指定したいですし簡単なので手動で 加えます。
MainFrm.hに

protected:
    BYTE        m_alpha;

を加えてください。 さらに、以下のようにMainFrm.cppのコンストラクタのイニシャライザを作成してください。

CMainFrame::CMainFrame() : m_alpha(255)
{
}

<メニューコマンドのハンドラ作成>
後で説明しますが、レイヤードウィンドウが表示されたのを確認するためにメニューに コマンドボタンとそのハンドラを作成します。
VSのメニューの表示(V)→その他のウィンドウ(E)→リソースビュー(R)でリソースビューを表示して、 「Menu」のIDR_MAINFRAMEを選択してメニューのリソースを表示してください。

上図のようにメニューの適当な位置に「CommandMenu」項目を作成して、その下に 「Command1」コマンドを作成してください。できたら「Command1」を選択したまま、 編集(E)→メニュー(M)→イベントハンドラの追加(A)で イベントハンドラウィザードを起動してください。

メッセージの種類・・・・・COMMAND
関数ハンドラ名・・・・・・(そのまま)
クラスの一覧・・・・・・・CMainFrame

で「追加して編集」を押下してください。

以上を実行すると、下記が追加されていると思います。

// MainFrm.h : CMainFrame クラスのインターフェイス
//  ・
class CMainFrame : public CFrameWnd
{
//  ・
//  ・
protected:
    BYTE        m_alpha;                        //Layered Windowのα値(透明度0〜255)

public:
    afx_msg void OnClose();                     //Window破壊シグナル(ここでレイヤード実行)
    afx_msg void OnTimer(UINT_PTR nIDEvent);    //フェードアウト用
    afx_msg void OnCommandmenuCommand1();
};
// MainFrm.cpp : CMainFrame クラスの実装
// ・
// ・

//メッセージマップ
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
// ・
// ・
    ON_WM_TIMER()
    ON_WM_CLOSE()
    ON_COMMAND(ID_COMMANDMENU_COMMAND1, &CMainFrame::OnCommandmenuCommand1)
END_MESSAGE_MAP()

// CMainFrame コンストラクション
CMainFrame::CMainFrame() : m_alpha(255)
{
}

//Window 閉じる時
void CMainFrame::OnClose()
{
    CFrameWnd::OnClose();
}

//タイマ
void CMainFrame::OnTimer(UINT_PTR nIDEvent)   
{
    CFrameWnd::OnTimer(nIDEvent);
}

void CMainFrame::OnCommandmenuCommand1()
{
}

・AnimateWindowでのフェードアウト処理

AnimateWindowの定義は
BOOL AnimateWindow(DWORD dwTime, DWORD dwFlags); となっています。
dwTimeは、アニメーションの再生時間を指定し、dwFlagsは、アニメーションの種類を指定します。 逆にいうと第二引数で指定できるスライドやフェードインなど決まったアニメーション効果以外のことはできない ということです。何が指定できるかはMSDNで確認してください。このように制限はありますが使い方は超簡単。 CMainFrame::OnClose()に次の一行を加えてください。

void CMainFrame::OnClose()
{
    AnimateWindow(1000, AW_HIDE | AW_BLEND);
    CFrameWnd::OnClose();
}

ビルドしてウィンドウを表示した後閉じてみてください。フェードアウト効果が 確認できると思います。また、この関数をCMainFrame::OnCreateで使用すればウィンドウの オープン時にアニメーションが実行されます。

・SetLayeredWindowAttributesでのフェードアウト処理

SetLayeredWindowAttributesの定義は
BOOL SetLayeredWindowAttributes(COLORREF crKey, BYTE bAlpha, DWORD dwFlags);
です。

この関数は

  1. crKeyで指定した色を透過(この色だけ透明になりその下の背景が見えるようになる)できる
  2. bAlphaの値によってウィンドウ全体の透明度を変更できる

の2つの機能のどちらかまたは両方を実現できます。 1、2のどちらの機能を使うかはdwFlagsで指定します。

フェードアウトを実行するにはCMainFrame::OnTimerで この関数の2.の機能を使って徐々に透明化すれいいです。しかし、いきなりOnTimerにこの関数を 実装しても失敗します。API版のSetLayeredWindowAttributesのドキュメントをご覧になるとわかりますが、 対象のウィンドウの拡張ウィンドウスタイルにWS_EX_LAYEREDを加えないといけません。 どのように加えるかですが、MSDNには

  1. CMainFrame::Createの第七引数(dwExStyle)で指定する
  2. SetWindowLongのnIndex引数でGWL_EXSTYEを設定してdwNewLong引数に指定する

とありますが、MFCではフレームワークによって簡単にできます。

前者のCMainFrame::CreateはCWinAppの派生クラスのInitInstanceで呼ばれている CMainFrame::LoadFrame内で呼ばれています。さらにCreateはCWnd::CreateExを 呼び出しているのですが、その前に仮想関数PreCreateWindowが使われているのでここで 拡張ウィンドウスタイルを変更できます。
後者はウィンドウが構築されたあとで拡張ウィンドウスタイルを変更する場合に使用しますが、 MFCではMofifyStyleExがあるのでこれを使えばいいと思いますが、今回はウィンドウ構築前に WS_EX_LAYEREDを設定するので前者でやってみます。

準備段階でプロジェクトを作成した際、 アプリケーションウィザードで既にPreCreateWindowがオーバーライドされているのでここに加筆します。

// MainFrm.cpp : CMainFrame クラスの実装
BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)
{
    // ・
    // ・
    cs.dwExStyle |= WS_EX_LAYERED;
    // ・
    return TRUE;
}

あとはCFrameWnd::OnTimerにSetLayeredWindowAttributesを実装すればよさそう ですがそうもいきません。WS_EX_LAYEREDが設定されたウィンドウはデフォで透明なウィンドウです。 つまり、タスクバーには現れますが、デスクトップ上は何も変化がありません。
UpdateLayeredWindowのところで書きますが、画面の各ピクセルの表現には32ビットが割り当てられ、 R、G、Bに各8ビットずつ使われて残り8ビットが未使用となっています。ここは0になっていますが、 WS_EX_LAYEREDが設定されるとここが透明度(α値)として使われるため透明になってしまうんじゃないかと 考えていますが、真偽のほどは?????なのでご存知の方は教えてくださると幸いです。

話を元に戻します。透明になってしますので SetLayeredWindowAttributesで透明度0にしなければなりません。どこに記述するかですが、 ウィンドウ構築時に呼ばれるCMainFrame::OnCreateで実行します。このメンバ関数も アプリケーションウィザードで既に作成されているので次1行を加えるだけでいいです。

// MainFrm.cpp : CMainFrame クラスの実装
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    // ・
    // ・
    SetLayeredWindowAttributes(0, 255, LWA_ALPHA);

    return 0;
}

では、中心部分を書いていきます。ロジックは CMainFrame::OnCloseでタイマを起動して、CMainFrame::OnTimerが呼ばれるたびに徐々に 第二引数bAlphaの値を小さくしながらSetLayeredWindowAttributesを実行します。準備段階で 作成したCMainFrame::OnClose、CMainFrame::OnTimerに以下のように記述してください。

// MainFrm.cpp : CMainFrame クラスの実装
//Window破壊シグナル
void CMainFrame::OnClose()
{
    if (! SetTimer(1, 200, NULL) )
        CFrameWnd::OnClose();

    //AnimateWindow(1000, AW_HIDE | AW_BLEND);  !!AnimateWindowの実装はコメントアウト!!
    //CFrameWnd::OnClose();
}

//タイマ
void CMainFrame::OnTimer(UINT_PTR nIDEvent)
{
    if (nIDEvent == 1)  
    {
        if (m_alpha < 25)  
        {
            KillTimer(1);
            DestroyWindow();
        } else  
        {
            SetLayeredWindowAttributes(0, m_alpha, LWA_ALPHA);  
            m_alpha -= 25;  
        }
    }
    //CFrameWnd::OnTimer(nIDEvent);
}

これでできたので、ビルドして実行してください。 (ウィンドウ表示後、閉じてください)SetLayeredWindowAttributesは設定によっては特定の色を 透過することもできるのでウィンドウ全体でレイヤードを実行する分には非常に便利です。

次のUpdateLayeredWindowではウィンドウの一部だけ透明にしたり、影をつけたり、 色を変更できたりといろんなことができますが、これがまた難しいです。 AnimateWindowとSetLayeredWindowAttributesの時と同様の処理をUpdateLayeredWindowで 実装して基本的な使い方?を次に書いてみます。