C++でjava風リスナを実装する

前回いまさらwindowsプログラミング - while( c++ );のプログラムにリスナを追加してみます。

今日の目標

  • WindowListenerインターフェイスを実装した派生クラスをウィンドウに追加する。
  • WindowClosing()でSystem::Exit()を呼び出すと、プログラムが終了する。
class HogeWindowListener
    : public WindowListener
{
public:
    //×ボタンを押したときの処理
    void WindowClosing( WindowEvent& e )
    {
        //プログラム終了
        System::Exit( 0 );
        std::cout << "closing" << std::endl;
    }
};

int main()
{
    std::cout << "start" << std::endl;

    //デフォルトのプロシージャを使って、ウィンドウクラスの登録。
    System::DefRegisterClass();
	
    //ウィンドウの作成
    Frame frame;
    frame.AddWindowListener( WindowListenerPtr( new HogeWindowListener() ) );
    frame.Create( 200, 150 );				//w, h

    System::MessageLoop();

    std::cout << "end" << std::endl;
    return 0;
}

Eventクラス

まずはイベントクラスを用意します。とりあえずウィンドウプロシージャの引数をまとめただけです。

class Event
{
public:
    HWND hwnd;
    UINT msg;
    WPARAM wp;
    LPARAM lp;

public:
    Event( HWND hwnd, UINT msg, WPARAM wp, LPARAM lp )
        : hwnd( hwnd )
        , msg( msg )
        , wp( wp )
        , lp( lp )
    {}
};

WindowEventクラス、WindowListenerインターフェイス

上記のEventクラスを継承したWindowEventクラスとWindowEventを処理するためのWindowListenerインターフェイスを用意します。

class WindowEvent
    : public Event
{
public:
    WindowEvent( Event const& e )
        : Event( e.hwnd, e.msg, e.wp, e.lp )
    {}
};

class WindowListener
{
public:
    virtual ~WindowListener(){}
    virtual void WindowClosing( WindowEvent& e ) = 0;
};

Frameクラス

FrameクラスにWindowListenerリストを持たせます。自動的にdeleteしてもらうためにshared_ptrの実装6 - while( c++ );で作ったshared_ptrを使います。

#include <list>
#include "shared_ptr.h"

typedef shared_ptr< WindowListener > WindowListenerPtr;
typedef std::list< WindowListenerPtr > WindowListenerList;

class Frame
{
private:
    HWND hwnd;
    WindowListenerList window_listener;

/*...*/

public:
    void AddWindowListener( WindowListenerPtr const& listener );
};
void Frame::AddWindowListener( WindowListenerPtr const& listener )
{
    window_listener.push_back( listener );
}

Systemクラス

プログラムを終了させるためのExit()を追加します。

class System
{
/*...*/

public:
    static void Exit( int exit_code );
};
void System::Exit( int exit_code )
{
    //メッセージループを終了させる
    PostQuitMessage( exit_code );
}

さらに、各Frameに関連付けられたリスナを実行するために、System::WinProc()を改良します。

LRESULT CALLBACK System::WinProc( HWND hwnd, UINT msg, WPARAM wp, LPARAM lp )
{
    //ここでFrameのリスナを実行したい
    Event e( hwnd, msg, wp, lp );
    frame->...( e );

    return DefWindowProc( hwnd, msg, wp, lp );
}

さて、ここでFrame*が必要になります。どこから持ってくれば良いのでしょう??


いろいろ解決法はあると思いますが、今回はCreateWindowEx()の引数からFrame*を取得しましょう。
CreateWindowEx()の最後の引数は、WM_CREATEのLPARAMとしてプロシージャに送られます。

bool Frame::Create( int w, int h )
{
    hwnd = CreateWindowEx
        ( 0
        , "hoge"
        , "title"
        , WS_OVERLAPPEDWINDOW
        , CW_USEDEFAULT
        , CW_USEDEFAULT
        , w
        , h
        , 0
        , 0
        , GetModuleHandle( 0 )
        , this         //←WinProc()に送る
        );
    if( hwnd == 0 )return false;

    ShowWindow( hwnd, SW_SHOW );

    return true;
}
LRESULT CALLBACK System::WinProc( HWND hwnd, UINT msg, WPARAM wp, LPARAM lp )
{
    if( msg == WM_CREATE )
    {
        //CreateWindowEx()の最後の引数(Frame*)を受け取る
        LPCREATESTRUCT cs = reinterpret_cast< LPCREATESTRUCT >( lp );
        Frame* frame = reinterpret_cast< Frame* >( cs->lpCreateParams );
        
        if( frame )
        {
            /*...*/
        }
    }

    /*...*/
}

しかし、このままではWM_CREATE時しかFrame*を使うことが出来ないので、どこかに保持しておく必要があります。ウィンドウが複数ある場合、hwndに対応するFrame*が必要になるのでstd::map< HWND, Frame* >で管理しましょうか。
とりあえず、Systemクラスのstaticメンバにしておきます。

#include <map>

class Frame;

class System
{
/*...*/

private:
    static std::map< HWND, Frame* > frame_map;
    static void Add( HWND hwnd, Frame* frame );
    static void Remove( HWND hwnd );
    static Frame* Search( HWND hwnd );
public:
    static size_t GetFrameCount()
    { return frame_map.size(); }
};
std::map< HWND, Frame* > System::frame_map;

LRESULT CALLBACK System::WinProc( HWND hwnd, UINT msg, WPARAM wp, LPARAM lp )
{
    if( msg == WM_CREATE )
    {
        //CreateWindowEx()の最後の引数(Frame*)を受け取る
        LPCREATESTRUCT cs = reinterpret_cast< LPCREATESTRUCT >( lp );
        Frame* frame = reinterpret_cast< Frame* >( cs->lpCreateParams );
        //ウィンドウハンドルをキーとするFrame*マップに追加
        if( frame )
            System::Add( hwnd, frame );
    }

    //ウィンドウハンドルをキーとしてFrame*を検索
    Frame* frame = System::Search( hwnd );
    if( frame )
    {
        //Frameクラスが持つオーバーライド可能なプロシージャを呼び出す
        Event e( hwnd, msg, wp, lp );
        frame->Proc( e );

        //ウィンドウが破棄された場合、Frame*マップから削除
        if( msg == WM_DESTROY )
        {
            System::Remove( hwnd );
        }

        return e.ret;
    }

    return DefWindowProc( hwnd, msg, wp, lp );
}

void System::Add( HWND hwnd, Frame* frame )
{
    frame_map.insert( std::make_pair( hwnd, frame ) );
}

void System::Remove( HWND hwnd )
{
    frame_map.erase( hwnd );
}

Frame* System::Search( HWND hwnd )
{
    std::map< HWND, Frame* >::iterator it = frame_map.find( hwnd );
    if( it != frame_map.end() )
    {
        return it->second;
    }
    return 0;
}

後はリスナを呼び出せば目的は達成できますね。
ということで、Frameクラスに個別のプロシージャを追加しましょう。
ちょと長くなりましたが、もう少しです。

class Frame
{
/*...*/
    ~Destroy()
    {
        Destroy();
    }
     
    void Destroy()
    {
        DestroyWindow( hwnd );
    }

public:
    void Default( Event& e );
    virtual void Proc( Event& e );

protected:
    void ProcessWindowEvent( WindowEvent& e );
};
void Frame::Proc( Event& e )
{
    if( e.msg == WM_CLOSE
        /* || その他のWindowEvent */
        )
    {
        ProcessWindowEvent( WindowEvent( e ) );
        //イベントが消費されていればすぐに終了
        if( e.IsConsumed() )return;
    }

    //デフォルトのプロシージャを実行
    Default( e );
}

void Frame::ProcessWindowEvent( WindowEvent& e )
{
    if( e.msg == WM_CLOSE )
    {
        struct closing
        {
            WindowEvent& e;
            closing( WindowEvent& e ) : e( e ){}
            void operator()( WindowListenerPtr const& listener )
            {
                e.Consume();	//イベントが消費された
                listener->WindowClosing( e );
            }
        };
        std::for_each
            ( window_listener.begin()
            , window_listener.end()
            , closing( e )
            );
    }

    /* その他のWindowEvent */
}

void Frame::Default( Event& e )
{
    e.ret = DefWindowProc( e.hwnd, e.msg, e.wp, e.lp );
}

イベント制御用にメンバを追加しました

class Event
{
/*...*/
public:
    Frame* frame;		///<	イベントの発生元Frame
    LRESULT ret;		///<	DefWindowProc()の戻り値を保持
    bool consumed;		///<	イベントが消費されたかどうか
};

完成


class HogeWindowListener
    : public WindowListener
{
public:
    //×ボタンを押したときの処理
    void WindowClosing( WindowEvent& e )
    {
        //プログラム終了
        System::Exit( 0 );
        std::cout << "closing" << std::endl;
    }
};

int main()
{
    std::cout << "start" << std::endl;

    //デフォルトのプロシージャを使って、ウィンドウクラスの登録。
    System::DefRegisterClass();
	
    //ウィンドウの作成
    Frame frame;
    frame.AddWindowListener( WindowListenerPtr( new HogeWindowListener() ) );
    frame.Create( "title", 200, 150 );		//title, w, h

    System::MessageLoop();

    std::cout << "end" << std::endl;
    return 0;
}


リスナを実装したFrame派生クラスも可能です。
ただし、shared_ptrにthisを渡すことになるので、deleteさせないためのdeleterを渡す必要があります。

class HogeFrame
    : public Frame                //extends
    , public WindowListener       //implements
{
    static void not_delete( WindowListener* ){}

public:
    HogeFrame()
        : Frame()
    {
        AddWindowListener( WindowListenerPtr( this, not_delete ) );
    }

public:
    void WindowClosing( WindowEvent& e )
    {
        System::Exit( 0 );
        std::cout << "closing" << std::endl;
    }
};

int main()
{
    std::cout << "start" << std::endl;

    //デフォルトのプロシージャを使って、ウィンドウクラスの登録。
    System::DefRegisterClass();
	
    //ウィンドウの作成
    HogeFrame frame;
    frame.Create( "title", 200, 150 );			//title, w, h

    System::MessageLoop();

    std::cout << "end" << std::endl;
    return 0;
}