◆フック & DLL - VC++ 2005 その3

[ ホーム ] [ 上へ ]

・実例 〜 グローバルフック - VC++ 2005

最後にグローバルフックの例として、マウスフックの簡単な例を掲載します。 マウスを右クリックしたら、そのウィンドウが最大化されていたら元のサイズに、 そうでなければ最大化するようにします。グローバルフックなのでデスクトップ上のすべての ウィンドウで右クリックによる振る舞いが変更されますのでその効果を確認してください。

<DLLプロジェクト作成>

前に解説したようにグローバル フックではフックプロシージャはDLLに書かないといけません。一方、このフックプロシージャを 利用するため(フックプロシージャをインストールする)にはSetWindowsHookExを コールしなければなりませんが、これはDLLでなくてもよいです。
しかし、フックプロシージャがあるDLL以外にSetWindowHookExを記述しようとすると、その前に DLLをロードしてフックプロシージャへのポインタを取得する必要があります。 (DLLのリンク参照)
ということで、SetWindowsHookExのコールを含めてフック関連のコードをDLL内の クラスにまとめてみます。

まず、以下のようにDLLプロジェクトを作成してください。
ツールーバーのファイル(F)→新規作成(N)→プロジェクト(P)の「新しいプロジェクト」 ダイアログボックスで以下のように入力してください。

プロジェクトの種類(P)   :  Win32
テンプレート(T)         :  Win32プロジェクト
プロジェクト名          :  GHook(任意)
後はデフォルトで→OK

続いて起動されるアプリケーションウィザードで以下のように入力してください。

(アプリケーションウィザードへようこそ)
→次へ

(アプリケーションの設定)
アプリケーションの種類      :  DLL
追加のオプション            :  シンボルのエクスポートにチェック
→完了

これでGHook.hとGHook.cppにスケルトンが書かれています。ヘッダファイルには DLLのエクスポートスイッチが書き込まれ、 実装ファイルにはDLLのエントリポイントDllMainが定義されています。まず、この2つのファイルを 修正します。

// ------------- GHook.h ---------------
#ifdef GHOOK_EXPORTS
#define GHOOK_API __declspec(dllexport)
#else
#define GHOOK_API __declspec(dllimport)
#endif

ヘッダファイルは上記の エクスポートスイッチ以外はコメントアウト または削除してください。declspec(dllexport)が有効になっているので、GHOOK_EXPORTSが 定義されているはずですがコード上では見つけられません。実は、cl.exeのオプションで マクロ指定しているのですが以下で確認してください。

ソリューションエクスプローラでGHookプロジェクトを選択して右クリックで プロパティ(R)を選択してGHookプロジェクトのプロパティページを表示させてください。

構成(C)     :  構成プロパティ
                   - C/C++
                       -   プリプロセッサ

ここのプリプロセッサの定義にGHOOK_EXPORTSを含めてマクロ定義があるので 確認してください。
次にGHook.cppです。

// ------------- GHook.cpp ---------------
// GHook.cpp : DLL アプリケーションのエントリ ポイントを定義します。
//

#include "stdafx.h"
#include "GHook.h"

#ifdef _MANAGED
#pragma managed(push, off)
#endif

//DLLインスタンスハンドル----------
HINSTANCE		g_hDllInst = NULL;
//---------------------------------

//DLLエントリポイント
BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
					 )
{
	switch (ul_reason_for_call)
	{
	case DLL_PROCESS_ATTACH:
		// DLLインスタンスハンドルをg_hDllInstに代入
		g_hDllInst = (HINSTANCE)hModule;				break;

	case DLL_THREAD_ATTACH:
	case DLL_THREAD_DETACH:
	case DLL_PROCESS_DETACH:
		break;
	}
    return TRUE;
}

#ifdef _MANAGED
#pragma managed(pop)
#endif

フックをセットするときにコールするSetWindowsHookExの第三引数には フックプロシージャがあるDLLのインスタンスハンドルを指定します。この値を保持するために、 DllMainで渡されてくるhModuleをグローバル変数に代入しています。

次に本題のフックを管理するクラスを作成します。といっても外部から このクラスのメンバにアクセスすることになり、 エクスポートしなければならないので 注意点で書いたように インターフェイスクラスと実装クラスに分離します。まず、インターフェイスクラスを 以下のように作成してください。

ツールバーのプロジェクト(P)→クラスの作成(C)で「クラスの追加」ダイアログを 表示して次のようにクラスを作成してください。

カテゴリ        :  C++
テンプレート    :  C++クラス

→追加

クラス名    :  CMouseHook

→完了

CMouseHookクラス(インターフェイス)のスケルトンが出来ているので 以下のように修正・加筆してください。

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

#include "GHook.h"                          // GHOOK_APIエクスポートスイッチ

//マウスフック用<インターフェイス>クラス
class /*GHOOK_API*/ CMouseHook
{
public:
    virtual BOOL Start()        = 0;        //フックスタート
    virtual BOOL End()          = 0;        //フック終了
    virtual void Destroy()      = 0;        //インスタンス破壊

    static GHOOK_API CMouseHook * Create();	        //CMouseHookインスタンス作成
};
// --------------- MouseHook.cpp - 実装ファイル--------------
#include "StdAfx.h"
#include "MouseHookImpl.h"	//MouseHook.hはインクルードされている

//CMouseHookインスタンス作成
/*static*/ CMouseHook *CMouseHook::Create()
{
    return      new CMouseHookImpl;
}

ヘッダファイルはフック管理クラス(CMouseHook)のフック開始、フック終了、 インスタンス破棄の各関数を純粋仮想関数で宣言しています。これらがインターフェイスを構成する 関数ですが、CMouseHook::Createはstaticで宣言しているので実際はCMouseHookのメンバ関数では ありません。グローバル関数で宣言・定義してもいいですが、CMouseHookのインスタンスを作成する ための関数なのでstaticで宣言しています。なお、このクラスはデータメンバも持っていなくて、 さらに純粋仮想関数があり抽象クラスなのでインスタンスは作成できません。したがって、 コンストラクタもデストラクタも定義する必要はないでしょう。

また、エクスポートについてですが、このクラスをエクスポートしなければ なりませんが、CMouseHook::Create以外は仮想関数でdeclspec(export)で修飾しなくても エクスポートできてしまうので (仮想関数のエクスポート参照) クラスのエクスポートスイッチ(GHOOK_API)をコメントアウトして、CMouseHook::Createのみに エクスポートスイッチを付けています。

実装ファイルはCMouseHook::Createの実装を行っています。CMouseHookの 実装クラスであり、派生クラスであるCMouseHookImpl(後でコーディングします)のインスタンスを 作成して、CMouseHookへのポインタにアップキャストして返却しているだけです。通常、 インターフェイスクラスには実装はないですが、この関数はstatic関数で、CMouseHookのメンバでは ないのでここで実装しています。

それではCMouseHookを派生させて実装クラスであるCMouseHookImplを作成します。 C++クラスウィザードを起動して、以下のように C++クラスを作成してください。

クラス名(L)         :  CMouseHookImpl
基本クラス(B)       :  CMouseHook
仮想デストラクタ(V) :  チェックする
→完了

スケルトンができたら以下のように修正・加筆してください。

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

#include "MouseHook.h"


//マウスフック用<実装>クラス
class CMouseHookImpl : public CMouseHook
{
public:
    CMouseHookImpl();                   //コンストラクタ
    virtual ~CMouseHookImpl();          //デストラクタ

//メンバ関数/////////////////////////////////////////////

    /*virtual*/ BOOL Start();           //フックスタート
    /*virtual*/ BOOL End();             //フック終了
    /*virtual*/ void Destroy();         //インスタンス破壊

private:
    static LRESULT CALLBACK MouseHookProc(int nCode,        //マウスフックプロシージャ
                                          WPARAM wParam,
                                          LPARAM lParam);

//メンバ変数/////////////////////////////////////////////
private:
    static HHOOK        m_hHook;        //フックハンドル
};
// ------------------- MouseHookImpl.cpp - 実装ファイル -------------
#include "StdAfx.h"
#include "MouseHookImpl.h"

//DLLインスタンスハンドルextern宣言
extern HINSTANCE    g_hDllInst;

//------------------------------------------------------------------------
//名前付データセグメント宣言
#pragma data_seg(".MHShare")
HHOOK           CMouseHookImpl::m_hHook = NULL;     //フックハンドル初期化
#pragma data_seg()

//.MHShareセクションを読み・書き・共有に属性を変更(システムグローバル)
#pragma comment(linker, "/SECTION:.MHShare,RWS")    //defファイルでも指定可
//------------------------------------------------------------------------

//コンストラクタ
CMouseHookImpl::CMouseHookImpl()
{
}
//デストラクタ
CMouseHookImpl::~CMouseHookImpl()
{
    End();
}

//Mouseフックスタート
//戻値	フック成功時TRUE
BOOL CMouseHookImpl::Start()
{
    if (m_hHook)
        ::UnhookWindowsHookEx(m_hHook);

    m_hHook = ::SetWindowsHookEx(WH_MOUSE,
                                 CMouseHookImpl::MouseHookProc,
                                 g_hDllInst,
                                 0);

    return	m_hHook ? TRUE : FALSE;
}

//フック終了
BOOL CMouseHookImpl::End()
{
    BOOL        bRet = TRUE;
    if (m_hHook)
    {
        bRet = ::UnhookWindowsHookEx(m_hHook);          m_hHook = NULL;
    }

    return bRet;
}

//CMouseHookImplインスタンス破壊
void CMouseHookImpl::Destroy()
{
    delete this;    //デストラクタでCMouseHookImpl::Endが呼ばれる
}

//マウスフックプロシージャ
//引数  nCode   フックコード(HC_ACTION or HC_NOREMOVE)
//      wParam  マウスメッセージ
//      lParam  MOUSEHOOKSTRUCT構造体へのポインタ
/*static*/
LRESULT CALLBACK CMouseHookImpl::MouseHookProc(int nCode,
                                               WPARAM wParam,
                                               LPARAM lParam)
{
    // nCodeが負 or メッセージキューからメッセージが削除されていない
    if (nCode < 0 || nCode == HC_NOREMOVE)
        return      ::CallNextHookEx(m_hHook, nCode, wParam, lParam);

    //右ボタンの場合
    if (wParam == WM_RBUTTONUP)
    {
        //マウスデータ
        LPMOUSEHOOKSTRUCT   pMhs = reinterpret_cast<LPMOUSEHOOKSTRUCT>(lParam);
        //右クリックした時のウィンドウのルートウィンドウ
        HWND                hWnd = ::GetAncestor(pMhs->hwnd, GA_ROOT);

        if (hWnd)
            ::ShowWindow(hWnd, ::IsZoomed(hWnd) ? SW_RESTORE : SW_MAXIMIZE);
    }

    return ::CallNextHookEx(m_hHook, nCode, wParam, lParam);
}

インターフェイスクラスCMouseHookを継承したCMouseHookImplでフックの セット(Start)、フックの解除(End)、フックプロシージャ(MouseHookProc)、つまりフック関連の すべての実装をしています。特にコメントしたい点は、 変数の共有化の部分です。ここでは フックプロシージャに注目してください。グローバルフックなのでデスクトップ上の すべてのスレッドでフックプロシージャ(MouseHookProc)がシステムによってコールされます。 ですから、ここで使用される変数に関しては注意してください。各スレッドで独立している場合は いいですが、この例のように少なくともフックハンドル(m_hHook)は共有化しないといけません。 ですから、.MHShareセクションを作成してそこにm_hHookの値を格納し、セクションの属性を 共有化しています(#pragma data_seg)。
また、フックプロシージャはコールバック関数なのでグローバル関数、またはクラスのstatic関数に する必要があるので注意してください。

フック処理をインターフェイスクラスと実装クラスに分離したので、もし、変更が あった場合でも、CMouseHookImplを変更している分には、利用するexe側はリビルドする必要は なく、単にdllをスワップすれば問題ないです。逆に言うと、CMouseHookクラスを変更したら、 dllを利用しているexeはリビルドしなければなりません。

フック関連をインターフェイスクラスと実装クラスに分離することによって カプセル化が強固になります。このdllを使うexeにはdll本体、CMouseHook.h、GHook.libの 3つファイルを配布すればよく、実装関連は一切外部に提示する必要はありません。

<dllを利用するexeプロジェクト作成>

以上でDLLの記述は済みました。あとはこれを使えばいいだけですが、DLLは単独で 実行できないので、これをロードして利用するEXEプロジェクトを作成します。DLLをリンクして CMouseHookクラスを使えばいいのでどんなEXEでもいいのですが、できるだけ簡単に作成するために MFCのダイアログベースのプロジェクトを作成します。
ツールバーのファイル(F)→新規作成(N)→プロジェクト(P)で「新しいプロジェクト」ダイアログを 表示して以下のように入力してください。

プロジェクトの種類    :    Visual C++
                                      - MFC
テンプレート          :    MFCアプリケーション
プロジェクト名        :    testHook(任意)
ソリューション        :    ソリューションに追加
→OK

MFCアプリケーションウィザードが起動しますので次のように入力してください。

(MFCアプリケーションウィザードへようこそ)
→次へ

(アプリケーションの種類)
アプリケーションの種類      :  ダイアログベース
→次へ

(ユーザーインターフェイス機能)
メインフレームのスタイル    :  システムメニューのみにチェック
→次へ

(高度な機能)
→次へ

(生成されたクラス)
→完了

次にリソースビューからダイアログエディタをを開いて以下のように ダイアログを作成してください。

番号 コントロールタイプ ID
[1]ボタンコントロールIDC_BTN_START
[2]ボタンコントロールIDC_BTN_END
[3]スタティックコントロールIDC_STC_STATUS
[4]ボタンコントロールIDOK

ダイアログリソースができたら、アプリケーションウィザードで作成された スケルトンの内、ダイアログのヘッダ・実装ファイルを以下のように加筆・修正してください。

// testHookDlg.h : ヘッダー ファイル
//

#pragma once

//------------------------------------
class CMouseHook;       //前方宣言
//------------------------------------

// CtestHookDlg ダイアログ
class CtestHookDlg : public CDialog
{
// ・
// ・
// ----------------- メンバ変数 ----------------------------
protected:
    CMouseHook      *m_pMouseHook;          //マウスフック管理クラス

// ----------------- メンバ関数 ----------------------------
protected:
    virtual void OnOK();                    //OKボタン
    virtual void OnCancel();                //Cancelボタン

public:
    afx_msg void OnBnClickedBtnStart();     //フック開始ボタン
    afx_msg void OnBnClickedBtnEnd();       //フック終了ボタン
};
// testHookDlg.cpp : 実装ファイル
//

#include "stdafx.h"
#include "testHook.h"
#include "testHookDlg.h"

#include "../GHook/MouseHook.h"

#ifdef _DEBUG
#pragma comment(lib, "../debug/GHook.lib")
#else
#pragma comment(lib, "../release/GHook.lib")
#endif


#ifdef _DEBUG
#define new DEBUG_NEW
#endif


// CtestHookDlg ダイアログ

CtestHookDlg::CtestHookDlg(CWnd* pParent /*=NULL*/)
    : CDialog(CtestHookDlg::IDD, pParent)
    , m_pMouseHook(NULL)
{
	m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}

// ・
// ・

BEGIN_MESSAGE_MAP(CtestHookDlg, CDialog)
// ・
// ・
    ON_BN_CLICKED(IDC_BTN_START, &CtestHookDlg::OnBnClickedBtnStart)
    ON_BN_CLICKED(IDC_BTN_END, &CtestHookDlg::OnBnClickedBtnEnd)
END_MESSAGE_MAP()


// CtestHookDlg メッセージ ハンドラ

BOOL CtestHookDlg::OnInitDialog()
{
    CDialog::OnInitDialog();
// ・
// ・

    m_pMouseHook = CMouseHook::Create();

    return TRUE;  // フォーカスをコントロールに設定した場合を除き、TRUE を返します。
}


// CDialog::OnOKオーバーライド
void CtestHookDlg::OnOK()
{
    if (m_pMouseHook)
    {
        m_pMouseHook->Destroy();            m_pMouseHook = NULL;
    }
    		
    CDialog::OnOK();
}

// CDialog::OnCancelオーバーライド
void CtestHookDlg::OnCancel()
{
    // CDialog::OnCancel();
}

//フック開始
void CtestHookDlg::OnBnClickedBtnStart()
{
    SetWindowPos(&wndTopMost, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
    if (m_pMouseHook)
    {
        if ( m_pMouseHook->Start() )
            GetDlgItem(IDC_STC_STATUS)->SetWindowText(_T("フック実行中・・・"));
        else
            GetDlgItem(IDC_STC_STATUS)->SetWindowText(_T(""));
    }
}

//フック終了
void CtestHookDlg::OnBnClickedBtnEnd()
{
    SetWindowPos(&wndNoTopMost, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
    if (m_pMouseHook)
        m_pMouseHook->End();

    GetDlgItem(IDC_STC_STATUS)->SetWindowText(_T(""));
}

以上ですべてできたので、ツールバーのビルド(B)→ソリューションのビルド(B)で 2つのプロジェクトをビルドしたのち、testHook.exeを実行して「フック開始ボタン」を 押下してフックを起動してください。デスクトップ上のどのウィンドウを右クリックしても ウィンドウの最大化⇔元に戻すがされるはずです。

参考文献:  Hooks
            フックの仕組み
            DLL の中のC++クラスの安全なエクスポート