◆邪魔な窓 - VC++ 2005 MFC

[ ホーム ] [ 上へ ]

タイトルだけでは何のことかわかないと思います。多くの人はPCを使用する時、 狭い画面で開発をしたり、インターネットを利用していると思います。中には デュアルディスプレイで贅沢な環境の方もいらっしゃるでしょうがほんの一部の方だと思います。

いくつもウィンドウを開いてデスクトップが手狭になってくると、 大抵は以前のものは閉じない限り、下に隠れていきますが、アプリケーションによっては常に 最前面に表示する場合があります。
当方もDesktopClockをそのように使用しています。 背景は透明なのですが、それでも針やルーラーが邪魔になる場合があります。ましてや 透明化してないウィンドウでは邪魔になる場面が多々あると思います。

そこで今回はマウスカーソルをウィンドウに合わせたら 一時的に非表示、又は透明化できるようにしてみます。ただ、マウスを当てた瞬間に ウィンドウが消えてしまっては、そのウィンドウに対して何かしたいときに非常に不便ですし、 再表示処理とでちらつきが発生してうっとうしいので、ある一定 時間が経過したら非表示(透明化)するようにしてみます

マウスをウィンドウに合わせたら非表示するだけでしたらそんなに難しくないです。 WM_NCHITTESTとかWM_SETCURSORメッセージのハンドラにShowWindow(SW_HIDE)を記述するだけで できるかと思います。一定時間経過したら消えるようにするために最初に思いつくのが SetTimerを使うことです。

この場合、マウスをウィンドウに合わせてSetTimerを発行して一定期間後に ウィンドウを消去しますが、その期間内にウィンドウを非表示にしたくなくてマウスをウィンドウ外に 移動した場合でも非表示になってしまします。こういう用途にうってつけのAPIがあります。

・TrackMouseEvent

対象のウィンドウに合わせてからそのウィンドウ上にマウスが一定期間存在 (ホバリング)した場合にメッセージを発行してくれる便利?なAPIがTrackMouseEventです。 定義は

BOOL TrackMouseEvent(
     LPTRACKMOUSEEVENT lpEventTrack  // TRACKMOUSEEVENT 構造体へのポインタ
);

で、TRACKMOUSEEVENT構造体の設定にしたがって一定期間後にメッセージを 発行してくれます。この構造体の設定によってはマウスがウィンドウから離れた時にもメッセージを 発行してくれるのでウィンドウの再表示にも都合がいいです。

肝心の構造体の定義は

typedef struct tagTRACKMOUSEEVENT {
    DWORD    cbSize;            //この構造体のサイズ
    DWORD    dwFlags;           //発行するメッセージの種類
    HWND     hwndTrack;         //マウスを追跡する対象のウィンドウハンドル
    DWORD    dwHoverTime;       //ホバリングの時間(WM_MOUSEHOVER発行までの時間)
} TRACKMOUSEEVENT, *LPTRACKMOUSEEVENT;

です。

メンバ 設定値
cbSize TRACKMOUSEEVENT構造体のサイズを指定。 sizeof(TRACKMOUSEEVENT)
dwFlags 発行するメッセージの種類を指定。
TME_HOVER マウスがdwHoverTimeの期間ウィンドウ上に停滞(ホバリング)していた場合にシステムより WM_MOUSEHOVERメッセージがhwndTrackのウィンドウに届けられます。

TME_HOVERが設定されたTrackMouseEventが既に発行されていて、dwHoverTime期間中に再度TME_HOVERで TrackMouseEventが発行されたときは前のdwHoverTimeの経過時間はリセットされます。

マウスがウィンドウ上にない状態で、TME_HOVERでTrackMouseEventを 発行してもWM_MOUSEHOVERは発生しません。
TME_LEAVE この値がdwFlagsに設定されている場合、ウィンドウ上にあったマウスがウィンドウ上から 離れた時すぐにWM_MOUSELEAVEがウィンドウに届けられます。

WM_MOUSELEAVEメッセージが発行されたら、TME_HOVERが設定されていても以降の マウスの追跡(トラッキング)は無効になります。
TME_NONCLIENT TME_HOVER、TME_LEAVEの場合はウィンドウのクライアント領域に対しての動作ですが、 ウィンドウの非クライアント領域のホバリングにはこの値を設定します。

キャプションバーなどの非クライアント領域にdwHoverTimeの期間ホバリングした場合、 WM_NCMOUSEHOVERWM_NCMOUSELEAVEメッセージが届けられます。
TME_CANCEL 既にTME_HOVERやTME_LEAVEを設定してTrackMouseEventが発行されている場合、再びこの値を 設定してTrackMouseEventを実行すると以前のマウスのトラッキング (WM_MOUSEHOVER、WM_MOUSELEAVEなどの発行指定)を中止できます。

例えば、最初にTME_HOVER | TME_LEAVEを指定してTrackMouseEventを実行し後、 TME_CANCEL | TME_HOVERで再度TrackMouseEventを実行すると、WM_MOUSEHOVERメッセージ発行 だけキャンセルされ、WM_MOUSELEAVEは領域からマウスが離れたらウィンドウに届けられます。
TME_QUERY 設定した内容をこの構造体に埋め込みます。
hwndTrack マウスのトラッキングをするウィンドウのハンドル(WM_MOUSEHOVERなどを届けるウィンドウハンドル
dwHoverTime WM_MOUSEHOVER、WM_NCMOUSEHOVER発行までの時間(1/1000秒単位)

TRACKMOUSEEVENT構造体の各メンバの設定内容は上表のとおりです。 説明だけではわかりづらいので例をあげます。

TRACKMOUSEEVENT        tme;

tme.cbSize      = sizeof(TRACKMOUSEEVENT);      
tme.dwFlags     = TME_HOVER | TME_LEAVE;      
tme.hwndTrack   = m_hWnd;      
tme.dwHoverTime = 3000;

::TrackMouseEvent(&tme);

m_hWndのハンドルを持つウィンドウのクライアント領域上にマウスがある状態で 上記の設定でTrackMouseEventを実行したとします。3秒以上その領域に留まっていた場合は m_hWndのウィンドウにWM_MOUSEHOVERメッセージが届けられます。そして、 ウィンドウ外にマウスがでたときはWM_MOUSELEAVEが発行されます。

・SystemParametersInfo

クライアント領域で一定期間マウスが留まっていたらWM_MOUSEHOVERが 発行されることはすでに書きましたが、実はマウスが留まってもよい領域 (WM_MOUSEHOVERが発行される領域)は、クライアント領域全体とは限らないです。

では、TrackMouseEventが発行されてからマウスが移動してもWM_MOUSEHOVERが 発行される領域はどこからどこまで?ということになりますが、それを調査/設定するのが SystemParametersInfoです。

::SystemParametersInfo(SPI_SETMOUSEHOVERWIDTH,  300, NULL, 0);
::SystemParametersInfo(SPI_SETMOUSEHOVERHEIGHT, 200, NULL, 0);

これでTrackMouseEventが発行されてからマウスが幅300ピクセル、高さ200ピクセルに 一定期間留まっていればWM_MOUSEHOVERメッセージが送られます。
クライアント領域全体にこの領域を設定するにはTrackMouseEventを発行する前にウィンドウの幅、 高さを調べてその倍の値をSystemParametersInfoで設定すればいいと思います。

・カレンダー

では、TrackMouseEventを使ってみたいのですがウィンドウを作成しないといけません。 今回はTrackMouseEventの効果が分かりやすいように、前準備として常に前面に表示される当月の カレンダを作成してみます。
その後にTrackMouseEventのロジックを実装します。仕様はカレンダ画面上に一定期間マウスが 留まったら(WM_MOUSEHOVERが発行されたら)、カレンダ画面を半透明にしてその下のウィンドウが 見えるようにします。そして、マウスがカレンダのクライアント領域から離れたら 再び非透明化して表示します

本当は半透明にしないで非表示または透明化したほうが、下の画面の操作ができるので 実践的ですが、カレンダが非表示、完全透明化した時点でWM_MOUSELEAVEメッセージが発行されるので、 再表示の実装を工夫しないといけないのでより単純な仕様にしました。

<プロジェクトの作成>

メニューのファイル(F)→新規作成(N)→プロジェクト(P)から

プロジェクトの種類(P) : Visual C++
テンプレート(T)       : MFC アプリケーション

を選択して任意のプロジェクト名を入力してください

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

アプリケーションの種類         : ダイアログベース
リソース言語                   : (デフォルト)
ユニコードライブラリを使用する : チェックする
プロジェクト形式               : (デフォルト)
MFCの使用法                    : (デフォルト)

メインフレームのスタイル       : システムメニューにのみチェックする
ダイアログタイトル            : (デフォルト)

高度な機能                     :(デフォルト)

最後にCWinAppとCDialogの派生クラス、CxxxApp、CxxxDlgクラスの名前を確認して 完了してください。2つクラスが作成されますが、以後CDialogの派生クラスは使用せずCWinAppの 派生クラスのみ使用します。

<カレンダクラスの作成>

CDialogの派生クラスを使用しない代わりにCWndの 派生クラスCCalenderを作成します。簡単に移動など出来ないようにキャプションバーがない ポップアップウィンドウを作ります。ツールバーのプロジェクト(P)→クラスの追加(C)で クラスウィザードを起動して以下のようにしてください。

カテゴリ     : MFC
テンプレート : MFCクラス
クラス名     : CCalender
基本クラス   : CWnd

完了するとスケルトンが作成されています。 それではカレンダ表示のための実装を以下のようにしてください。

  1. マウスホバー時間保持用UINT型メンバ変数 m_uiHoverTimeをprotecedアクセス制御で作成。
    メニューの表示(V)→クラスビュー(A)でCCalenderを選択後、プロジェクト(P)→変数の追加(B)から 追加してください。
  2. 自動後始末用 PostNcDestroy関数をオーバーライドします。
    同クラスビューのCCalenderを選択して右クリックからプロパティ(R)でプロパティウィンドウを表示して ツールバーのオーバーライドアイコンを選択してPostNcDestroy関数をオーバーライドしてください。
  3. ウィンドウ構築用Create関数をオーバーロードします。
    クラスビューでCCalenderを選択後プロジェクト(P)→関数の追加(U)で
    戻値の型   :  BOOL
    関数名     : Create
    パラメータ : CRect型         rect
                 UINT型          uiHoverTime
    アクセス   : public
    として作成してください。
  4. カレンダ描画用WM_PAINTメッセージハンドラ OnPaintの作成。
    2.のプロパティウィンドウからメッセージのアイコンを選択してWM_PAINTから OnPaint関数を作成 してください。

2.3.4の実装内容は以下のコードをご覧ください。

//Calender.h ヘッダファイル
#pragma once

// CCalender
class CCalender : public CWnd
{
    DECLARE_DYNAMIC(CCalender)

public:
    CCalender();
    virtual ~CCalender();

protected:
    UINT m_uiHoverTime;           //Hover時間

protected:
    virtual void PostNcDestroy(); //C++オブジェクト自動破棄

public:
    BOOL Create(CRect rect, UINT uiHoverTime); //カレンダウィンドウ構築
    afx_msg void OnPaint();                    //カレンダ描画

    DECLARE_MESSAGE_MAP()
};
// Calender.cpp : 実装ファイル
//
#include "stdafx.h"
#include "xxxxxxxxxxxx.h   //CWinAppの派生クラスのヘッダファイル名
#include "Calender.h"

// CCalender
IMPLEMENT_DYNAMIC(CCalender, CWnd)

//コンストラクタ
CCalender::CCalender() : m_uiHoverTime(0)
{
}

//デストラクタ
CCalender::~CCalender()
{
}

BEGIN_MESSAGE_MAP(CCalender, CWnd)
    ON_WM_PAINT()
END_MESSAGE_MAP()

//カレンダウィンドウ構築
//引数 rect        カレンダウィンドウ表示位置(スクリーン座標)
//     uiHoverTime 透明化までの時間(WM_MOUSEHOVER発行までの時間)
BOOL CCalender::Create(CRect rect, UINT uiHoverTime)
{
    CPoint    point;
    BOOL      bRet = FALSE;

    do
    {
        //Hover時間設定
        m_uiHoverTime = uiHoverTime;

        //Window構築
        if (! CreateEx(WS_EX_TOPMOST | WS_EX_LAYERED,
                       AfxRegisterWndClass(CS_DBLCLKS, 0,
                                           reinterpret_cast<HBRUSH>(COLOR_WINDOW + 1),
                                           0),
                      _T("Calender"),
                      WS_POPUPWINDOW | WS_VISIBLE,
                      rect,
                      NULL, 0)
        )
        break;

        //レイヤード設定
        if (! SetLayeredWindowAttributes(0, 255, ULW_ALPHA))    break;

        //Window更新
        UpdateWindow();

        //カーソル処理
        if (! GetCursorPos(&point) )                            break;

        GetWindowRect(&rect);
        if (! SetCursorPos(rect.left, rect.top))                break;
        if (! SetCursorPos(point.x, point.y))                   break;

        bRet = TRUE;
    } while(0);

    return bRet;
}

//C++オブジェクト自動破棄
void CCalender::PostNcDestroy()
{
    delete this;   
}

//カレンダ表示
void CCalender::OnPaint()
{
    CPaintDC dc(this);   // device context for painting
    // TODO: ここにメッセージハンドラコードを追加します。
    // 描画メッセージでCWnd::OnPaint() を呼び出さないでください。
    CRect     rect;
    int       nMonth = CTime::GetCurrentTime().GetMonth();
    CTime     start  = CTime(CTime::GetCurrentTime().GetYear(), nMonth, 1, 0, 0, 0); 
    int       width, height; 
    CPoint    point(0, 0); 
    CString   str; 
    TCHAR     *pszWeek[] = {_T("日"), _T("月"), _T("火"), _T("水"),
                            _T("木"), _T("金"), _T("土")
                           };

    GetClientRect(&rect);
    width = rect.Width() / 7;         height = rect.Height() / 8; 

    //月の表示
    str.Format(_T("%d月"), nMonth);
    dc.DrawText(str, CRect(point, CSize(rect.Width(), height)), DT_CENTER); 

    //曜日の表示
    point += CSize(0, height); 
    for (int i=0; i < sizeof pszWeek / sizeof pszWeek[0]; i++) 
        dc.DrawText(pszWeek[i], -1, CRect(point + CSize(i*width, 0),
                                          CSize(width, height)), DT_CENTER); 

    //各日付の表示
    point += CSize( (start.GetDayOfWeek() - 1) * width, height ); 

    for (CTime t = start; t.GetMonth() == nMonth; t += CTimeSpan(1, 0, 0, 0) ) 
    {
        if ( t.GetDayOfWeek() <= 1 ) 
            point = CPoint(0, point.y + height); 

        str.Format(_T("%d"), t.GetDay());
        dc.DrawText(str, CRect(point, CSize(width, height)), DT_CENTER); 

        point += CSize(width, 0); 
    }
}

これでCCalenderができたので、CWinAppの派生クラスCxxxxApp::InitInstanceでインスタンス化します。

アプリケーションウィザードでCxxxApp::InitInstanceの スケルトンが既にできていますが、CDialogの派生クラスを使うようになっているので以下のように コメントアウトした上、CCalenderに差し替えてください。

// TME.cpp : アプリケーションのクラス動作を定義します。
//
#include "stdafx.h"
#include "TME.h"
//#include "TMEDlg.h"     コメントアウトする
#include "Calender.h"      // 追加する

//    ・
//    ・
//    ・

// CTMEApp 初期化
BOOL CTMEApp::InitInstance()
{
    //  ・
    //  ・
    //  ・

    //CTMEDlg dlg;
    //m_pMainWnd = &dlg;
    //INT_PTR nResponse = dlg.DoModal();
    //if (nResponse == IDOK)
    //{
    // // TODO: ダイアログが<OK> で消された時のコードを
    // // 記述してください。
    //}
    //else if (nResponse == IDCANCEL)
    //{
    // // TODO: ダイアログが<キャンセル> で消された時のコードを
    // // 記述してください。
    //}

    CCalender *pCalender = new CCalender;
    if (pCalender)
        m_pMainWnd = pCalender;
    else
        return FALSE;

    // ダイアログは閉じられました。アプリケーションのメッセージポンプを開始しないで
    // アプリケーションを終了するためにFALSE を返してください。

    //return FALSE;
    return pCalender->Create(CRect(100, 100, 400, 300), 2000);
}

これで準備段階のカレンダ表示ができるはずなので、ビルドして実行してみてください。 以下のようなカレンダが表示されると思います。

このウィンドウは非クライアント領域がない(ボーダー部分はありますがほとんどないに 等しい)ので、移動にドラッグ&ドロップができなかったり、最小化のシステムメニューなしで ウィンドウを作っていて、最小化できなかったりするので、この下にウィンドウの一部があり、 それが隠れていしまっている場合たいへん困ります。

そこでこのセクションの最初に書いたように、CCalenderに一定時間マウスがカレンダ上に 留まれば、ウィンドウを半透明化し、離れたら再び非透明化するようにします。

・TrackMouseEventの使用

ずいぶん前置きが長くなってしまいましたがここからが本題です。 といってもコード量はそんなに多くないです。

  1. マウスがウィンドウに移動したら、TrackMouseEventを発行して条件がそろえば、WM_MOUSEHOVER、 WM_MOUSELEAVEをポストするにします。
    条件とは、そのままマウスがカレンダ上に一定期間留まればWM_MOUSEHOVER、マウスがウィンドウから 離れたらWM_MOUESLEAVEをポストすることを指します。
  2. WM_MOUSEHOVER、WM_MOUSELEAVEに対応するメッセージハンドラを作成して実装します。

コードの内容は上記の通りですが、「マウスがウィンドウに移動」を確認する 必要があります。spy++で調べるとWM_NCHITTEST、WM_SETCURSORが送られてきているのでOnNcHitTest、 OnSetCursorで処理すればいいと思います。

afx_msg UINT OnNcHitTest(
    CPoint point
);

afx_msg BOOL OnSetCursor(
    CWnd* pWnd,
    UINT  nHitTest,
    UINT  message
);

ただ、WM_MOUSEHOVER、WM_MOUSELEAVEはクライアント領域上のマウスに対して ポストされるので非クライアント上にいないことを確認しなければなりません。
OnNcHitTestでは戻値でその位置を返すので関数内では確認できません。一方、OnSetCursorは 第二引数のnHitTestでマウスの位置が渡されるので、この値を利用すればクライアント領域上か そうでないか判断できます。

ということで、このハンドラでTrackMouseEventを 実行することにします。実装内容はすべてCCalenderで次の通りです。

  1. TrackMouseEvent関数発行確認フラグの作成。
    OnSetCursorでTrackMouseEventを発行しますが、 TRACKMOUSEEVENT構造体で書いたように、一度発行して ホバー期間内に再びTrackMouseEventが実行されるとその経過期間はリセットされるので、 TrackMouesEventを実行したら、WM_MOUSEHOVERまたはWM_MOUSELEAVEがポストされるまで再度 実行されないようにフラグを立てます。
  2. WM_SETCURORのハンドラ、OnSetCursor関数の作成。
    OnPaintの時と同様に、クラスビューからメッセージアイコンを選択して WM_SETCURSORをさがしてハンドラを作成してください。
  3. WM_MOUSEHOVER、WM_MOUSELEAVEハンドラの作成。
    2.と同様にすればいいように思われますが標準のウィンドウメッセージではないので、クラスビュー からプロパティで表示されるメッセージ一覧にはないです。
    なので、自分でメンバ関数とメッセージマップの記述をしないといけません。メッセージマップの記述には ON_MESSAGEマクロを使いますが、記述方法は下記のコード及びMSDNを参照してください。
//Calender.h ヘッダファイル
// ・
// ・
class CCalender : public CWnd
{
    // ・
    // ・
protected:
    BOOL m_bHoverFlg;   //TrackMouseEvent Activeフラグ

public:
    //WM_SETCURSORハンドラ
    afx_msg BOOL OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message);
    //WM_MOUSEHOVER, WM_MOUSELEAVEハンドラ
    afx_msg LRESULT OnMouseHover(WPARAM wParam, LPARAM lParam);
    afx_msg LRESULT OnMouseLeave(WPARAM wParam, LPARAM lParam);

    //・
};
// Calender.cpp : 実装ファイル
// ・
// ・

//コンストラクタ
CCalender::CCalender() : m_uiHoverTime(0)
                       , m_bHoverFlg(FALSE)
{
}
// ・
// ・
BEGIN_MESSAGE_MAP(CCalender, CWnd)
    ON_WM_PAINT()
    ON_WM_SETCURSOR()
    ON_MESSAGE(WM_MOUSEHOVER, &CCalender::OnMouseHover)
    ON_MESSAGE(WM_MOUSELEAVE, &CCalender::OnMouseLeave)
END_MESSAGE_MAP()
// ・
// ・
BOOL CCalender::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message)
{
    //TrackMouseEventの実行確認 & マウスのクライアント上の存在確認
    if (! m_bHoverFlg && nHitTest == HTCLIENT)
    {
        do
        {
            CRect         rect;     //Windowのクライアント領域
            GetClientRect(&rect);

            //WM_MOUSEHOVERの発行を許可できるマウスの移動範囲の設定
            if (! ::SystemParametersInfo(SPI_SETMOUSEHOVERWIDTH,
                                                       rect.Width() * 2, 0, 0)
               )
                break;

            if (! ::SystemParametersInfo(SPI_SETMOUSEHOVERHEIGHT, 
                                                       rect.Height() * 2, 0, 0)
               )
                break;

            //WM_MOUSEHOVER, WM_MOUSELEAVEメッセージ発行許可
            TRACKMOUSEEVENT      tme;

            tme.cbSize      = sizeof(TRACKMOUSEEVENT);
            tme.dwFlags     = TME_HOVER | TME_LEAVE;
            tme.hwndTrack   = m_hWnd;
            tme.dwHoverTime = m_uiHoverTime;

            if (! ::TrackMouseEvent(&tme))
                break;

            m_bHoverFlg = TRUE;
        } while (0);
    }
    return CWnd::OnSetCursor(pWnd, nHitTest, message);
}

//MouseHoverハンドラ
LRESULT CCalender::OnMouseHover(WPARAM wParam, LPARAM lParam)
{
    return ! SetLayeredWindowAttributes(0, 50, ULW_ALPHA);
}

//MouseLeaveハンドラ
LRESULT CCalender::OnMouseLeave(WPARAM wParam, LPARAM lParam)
{
    m_bHoverFlg = FALSE;
    return ! SetLayeredWindowAttributes(0, 255, ULW_ALPHA);
}

以上でできたのでビルドしてカレンダを表示してください。上記の例では ウィンドウ上に2秒以上留まれば、半透明化されて下のウィンドウ(デスクトップ) が見えるようになるはずです。

今回はWM_MOUSELEAVEのポストのタイミングのために半透明化にしましたが、 下のウィンドウの処理をしたい場合は非表示にしないといけません。その際、再表示のためには グローバルフックなどでマウスメッセージを補足する必要がでてくるかもしれません。