OSが用意してくれているボタンやテキストボックスのラッパーとして以下のようなクラスを作ってみている。これはコントロールを作成してサブクラス化し、subclass_func_tファンクタにメッセージを転送するものである。SubClassFuncの内容をメンバ関数に転送するところはXBYAKでサンク(糊コードといったほうがいいのかな)コードを書いている。
namespace sf{
class control_base
{
public:
// サブクラス化した後に呼ばれるファンクタ定義
typedef std::function<LRESULT (control_base& base, UINT uMsg, WPARAM wParam, LPARAM lParam) > subclass_func_t;
// ポインタ型
typedef std::unique_ptr<control_base> control_base_ptr;
template <typename ParentWindowType>
control_base(std::wstring& class_name,
std::wstring& window_name,
subclass_func_t func,
ParentWindowType& parent,
int x = 0, // ウィンドウの横方向の位置
int y = 0, // ウィンドウの縦方向の位置
int nWidth = CW_USEDEFAULT, // ウィンドウの幅
int nHeight = CW_USEDEFAULT, // ウィンドウの高さ
HMENU hMenu = NULL, // メニューハンドルまたは子識別子
DWORD dwExStyle = 0,
DWORD dwStyle = WS_CHILD | WS_VISIBLE, // ウィンドウスタイル
HINSTANCE hInstance = GetModuleHandle(NULL) // アプリケーションインスタンスのハンドル
) : thunk_((LONG_PTR)this), func_(std::move(func))
{
hwnd_ = CreateWindowEx(dwExStyle,class_name.c_str(),window_name.c_str(),dwStyle,x,y,nWidth,nHeight,reinterpret_cast<HWND>(parent.raw_handle()),hMenu,hInstance,NULL);
if (hwnd_ == NULL){
throw sf::win32_error_exception();
}
proc_ = (SUBCLASSPROC) thunk_.getCode();
// サブクラス化
SetWindowSubclass(hwnd_, proc_, (UINT_PTR) &id_subclass_, NULL);
}
virtual ~control_base()
{
// サブクラス化解除
RemoveWindowSubclass(hwnd_, proc_, (UINT_PTR) &id_subclass_);
};
// ファンクタを呼び出すための転送メンバ関数
LRESULT subclass_proc_(HWND hWnd,UINT uMsg,WPARAM wParam, LPARAM lParam )
{
//return DefSubclassProc(hWnd,uMsg,wParam,lParam);
assert(hWnd == (HWND)this->raw_handle());
return func_(*this, uMsg, wParam, lParam);
}
void* raw_handle(){ return hwnd_; }
private:
UINT id_subclass_;
HWND hwnd_;
SUBCLASSPROC proc_;
subclass_func_t func_;
// メンバー関数を直接呼び出すサンクというかグルーコード。
struct subclassproc_thunk : public Xbyak::CodeGenerator {
subclassproc_thunk(LONG_PTR this_addr)
{
LRESULT(control_base::*pmemfunc)(HWND, UINT, WPARAM, LPARAM) = &control_base::subclass_proc_;
LONG_PTR proc = (LONG_PTR)(*(void**) &pmemfunc);
// 引数の位置をひとつ後ろにずらす
mov(r10, r9);
mov(r9, r8);
mov(r8, rdx);
mov(rdx, rcx);
// thisのアドレスをrcxに格納する
mov(rcx, (LONG_PTR) this_addr);
// 第5引数をスタックに格納
push(r10);
sub(rsp, 32);
mov(r10, proc);
// メンバ関数呼び出し
call(r10);
add(rsp, 40);
ret(0);
}
};
subclassproc_thunk thunk_;
protected:
};
}
このクラスは以下のように使う。
child_base_.reset(new sf::control_base(std::wstring(L"SFCHILD"), std::wstring(L"SFCHILD"),
[this](sf::control_base& base, UINT uMsg, WPARAM wParam, LPARAM lParam)->LRESULT
{
// メッセージに対応する処理をここに書く
return DefSubclassProc((HWND) base.raw_handle(), uMsg, wParam, lParam);
}, *this, 0, 0, 640, 480, (HMENU) 1, WS_EX_LAYERED, WS_CLIPSIBLINGS | WS_CHILD, HINST_THISCOMPONENT));
ファンクタへの転送にコストがかかっているけれども実用上は差支えないのでこれで行こうと思うのだが、1つだけ問題があってそれは、「子コントロールのWM_COMMANDメッセージは親ウィンドウに送られる」というOSの仕様である。ボタンが押されたというWM_COMMANDメッセージは、親ウィンドウに送られ、子ウィンドウには送られないのである。私は上記のラッパーで指定したファンクタ内でメッセージをハンドルしたいと考えてこのラッパーを書いた。理由は親ウィンドウのWM_COMMANDメッセージをハンドルするコードがUIが複雑になるにつれ条件分岐の塊と化していくのがとても嫌だからである。これでは意味がないではないか。。
少し考えて下記のようにすればいいのではないかと思いついた。WM_COMMANDメッセージを子ウィンドウに転送するのである。そうすれば子ウィンドウのファンクタ内でイベントを処理できる。実際に試すとちゃんと動いた。
こんな用途のAPIがありそうな気もするのでちょっと調べてみようかなとも思っている。