ホーム < ゲームつくろー! < Programming TIPs編
その10 目指せ究極のカスタムボタン!
Windowプログラムでボタンをウィンドウ内に設置するには、CreateWindow APIを用いてボタン(ボタンもウィンドウです)を作成します。とても簡単なのですがデフォルトのボタンのデザインは事務的で味気ない感じもします。Windowsプログラマなら誰しも一度は「自分がデザインしたボタンを押せたらなぁ」と思うはずなんです!・・・たぶん。そういう要求を反映してか、カスタムボタンの作り方を解説したサイトは世に本当に沢山存在します。
でも、何だか微妙なんです。例えば、とても典型的なのですが、カスタムボタンの絵をオーナードロー(親ウィンドウがボタンを描画(後述))させている記述が多く見受けられます。これ、正直「う〜〜ん」と思うわけです。デフォルトのボタンは作成すれば勝手に描画されるし、勝手に押されるのに、カスタムになったら親が描画。それじゃボタンとして統一感がありません。そもそも、グローバルな親プロシージャにカスタムボタンの根幹に関わる描画プロセスを記述した段階で、すでにカスタムボタンとしての汎用性を失っています。親はボタンを使うだけ、描画などはボタンに任せる。これが正しいカスタムボタンではないでしょうか?
そこでこの章では、他人様に極力迷惑をかけない究極的なカスタムボタン作ってみます。作るカスタムボタンのデザインはもちろん自身が描いた物を使え、しかも完全にクラス化され、且つ通常のボタンと同じメッセージを送受信できますので、親ウィンドウはいつもと同じようにボタンを扱う事ができます。
これまでボタンをダイアログでは無いウィンドウに表示させた事の無い方でも理解が出来るように、知識の下準備をこってりしっかりして参ります。では、いきますよ〜(^-^)
@ カスタムボタンの仕様
カスタムボタンは通常のデフォルトボタンと同じように押す事ができます。ただ当然ですがカスタムなので「押された」「離された」画像は別途用意する必要があります。カスタムボタンはデフォルトボタンとほぼ全く同様にウィンドウメッセージを送受信します。つまりマウスカーソルのメッセージ情報(WM_****)などをちゃんとキャッチできるし、親に対してメッセージを投げかける(WM_COMMAND)事もできます。もう1つ、カスタムボタンはその形状通りにボタンが押せます。例えば丸いボタンなら丸い部分の内側だけに反応すると言う事です。
以上の仕様を満たすには、色々と下準備知識が必要となります。一つ一つじっくりと説明致します。
A 下準備その1:通常のボタンをおさらい
カスタムボタンに取り掛かる前に、まずはデフォルトボタンの振る舞いをおさらいします。ここをしっかり押さえる事が大切なんです。
デフォルトボタンをウィンドウに配置するには、その親となるウィンドウを生成しなければなりません。さすがに親ウィンドウは生成できると思いますので生成方法の説明は割愛します。デフォルトボタン作成時に欲しいのは親ウィンドウのウィンドウハンドル(hParentWnd)とインスタンスハンドル(HINSTANCE)です。ただ、インスタンスハンドルはウィンドウハンドルから取得できます:
インスタンスハンドルの取得 HINSTANCE hInst = (HINSTANCE)(LONG64)GetWindowLong( hWnd, GWL_HINSTANCE );
(LONG64)というキャストは、最近のコンパイラが出すwarning(LONGをより大きな整数にするけどいいの?というワーニング)への対処です。これより、インスタンスハンドルは気にしなくても大丈夫です。
デフォルトボタンは例えば次のように生成できます:
デフォルトボタンの生成 HWND hButtonWnd = CreateWindow( _T("BUTTON"), NULL, WS_VISIBLE | WS_CHILD | BS_PUSHBUTTON,
10, 10, 50, 20, hParentWnd, (HMENU)BUTTON_ID, hInst, NULL );
各パラメータの詳細についてはMSDNを参照して下さい。難しい引数は何もありません。この生成が成功すると、ウィンドウには直ちにデフォルトボタンが表示され、マウスで押す事もできます。
生成後ボタンを押しても見た目は何も起こりません。押した後の振る舞いについてコードに書いていないので当然です。でも、実は裏では沢山の作用が起きています。生成したてのボタンを押したり離したりした時、ボタンを保持している親ウィンドウには「Notification message(通知メッセージ)」がどかどかと送り込まれています。ボタン特有の送信メッセージを列挙してみますと、
通知メッセージ タイミング BN_CLICKED ボタンクリックの瞬間 BN_DISABLE ボタンが使用不可になった時 BN_PUSHED ボタンを押した時 BN_KILLFOCUS ボタンからフォーカスが外れた時 BN_PAINT ボタンが描画された時 BN_SETFOCUS ボタンがキーボードによるフォーカスを得た時 BN_UNPUSHED ボタンがもはや押せない状態である時
などとなります。この他にもWM_COMMANDなども親は受け取ります。受け取り先は親のメッセージプロシージャ関数(通常グローバル関数)です:
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam){ ... }
ボタンを押した時、hWndには親ウィンドウのハンドルが、msgには「WM_COMAND」が入ってきます。この時wParamには「通知メッセージ+ボタンID」が送られてきます。wParamの下位16ビットがボタンID、上位16ビットが通知メッセージです。そしてlParamにはボタンのウィンドウハンドルが入ります。まとめると、上の4つの引数から次のような情報が得られます:
HWND hParentWnd = hWnd; // 親ウィンドウハンドル
UINT Message = msg; // WM_COMMAND
WORD ButtonID = LOWORD( wParam ); // ボタンID
WORD NotificationMSG = HIWORD( wParam ); // 通知メッセージ
HWND hButton = (HWND)lParam; // ボタンウィンドウハンドル
これだけの情報があれば「ボタンの状態が変化した時の振る舞い」を親プロシージャにこと細かく記述する事ができるわけです。
では、今度は目線を生成したボタンに向けてみましょう。ボタンは生成してすぐに押す事が可能ですし、上のような通知メッセージもバンバン発します。そういう「押された絵」とか「ボタンメッセージ」は、いったいどこの誰が制御しているのでしょうか?
実は、ボタンを生成すると裏でひっそりと「ボタンプロシージャ」が発動し始めます。ボタンプロシージャ(と言う名前かは良く知りませんが(^-^;)はWindowsが予め用意してくれているグローバルなウィンドウプロシージャの一つで、すべてのボタンの挙動を監視して、つぶさにメッセージを発したり各ボタンの描画を行ったりしています。知らない所でがんばってくれていた訳です。
しかしながら、カスタムボタンを作る際にはその頑張りが幾分サービス過剰になります。勝手にデフォルトボタンを描画されると困るわけです。とは言え、デフォルトのボタンプロシージャ自身は書き換えることができません。しかし、実はボタンプロシージャは自前のカスタムボタンプロシージャと取り替えることが出来ます。自前のプロシージャは書き換え自由ですから、描画部分も変更できます。これによりカスタムボタンが描画できるわけです。この手法を「サブクラス化」と言います。また、カスタムボタンを描画する時にはオリジナルの描画を止める必要もあります。これは冒頭に出てきた「オーナードロー」という方法が使えます。サブクラス化とオーナードロー。この2つの合わせ技で究極カスタムボタンができます(^-^)v
B 下準備その2:オーナードローは描画を止めるだけ
オーナードロー(Owner Draw)というのは、その名の通り「持ち主による描画」の事を言います。今回の場合、持ち主は親ウィンドウで、持っているものは「ボタン」です。ボタンをオーナードローモードにすると、デフォルトボタンの描画がキャンセルされます。変わりに本来描画される部分にはボタン背景色(灰色)が塗られます。
ボタンの描画をオーナードローにするには、ボタンを生成する時にBS_OWNERDRAWフラグを立てます:
オーナードローボタン |
hButtonWnd = CreateWindow( "BUTTON", NULL, WS_CHILD | WS_VISIBLE | BS_OWNERDRAW, 0, 0, 100, 100, hWnd, (HMENU)ID_MI, hInst ,NULL); |
ボタンらしい絵は描画されませんが、ボタン自体の機能は生きていて、背景色で塗りつぶされた場所でクリックするとちゃんとメッセージが送出されます。
ボタンをオーナードロー状態にして、親ウィンドウがボタンからのメッセージ(WM_COMMAND)を拾い、状態に対してボタン跡地に自ら描画すればカスタムボタンを確かに実現できます。でもその段階で親プロシージャのWM_COMMAND部分はいいだけ汚れてしまいます。デフォルトボタンの時にはそんな描画作業はありませんでした。であるならば「カスタムボタンだって描画しちゃいけない」。そう思うわけです。
ですから、オーナードローはあくまでもボタンの描画を止めるための措置として捉え、ボタンの描画自体は「ボタン自身」に行わせるのが、カスタムボタンとしてよりふさわしいかなと思います。そのためには、オーナードロー状態のボタンプロシージャを乗っ取るしかありません!
C 下準備その3:ボタンプロシージャの乗っ取り(ウィンドウのサブクラス化)
ボタンはそれを描画したりクリック状態を感知する「ボタンプロシージャ」をひっそりと持っています。カスタムボタンを作るにあたり、特に描画を自前でやりたいと考えています。そこで、デフォルトのボタンプロシージャを自前のボタンプロシージャに置き換えて機能を乗っ取ってしまいます。これを(ボタン)ウィンドウのサブクラス化と言います。
乗っ取る方法です。必要なのは「GetWindowLong関数」と「SetWindowLong関数」です。これらの関数にウィンドウハンドルを渡すと、それに対応するインスタンスハンドルやウィンドウプロシージャ関数ポインタなど大変にコアな情報を取得・設定する事ができます。取得はまだしも「設定」もできるというのがポイントです。
ボタンもウィンドウなので、ウィンドウハンドルを持っていて、GetWindowLong関数によってボタンプロシージャへのポインタを取得する事ができます。またSetWindowLong関数を通せば独自のボタンプロシージャ(カスタムプロシージャ)に入れ替える事もできます。カスタムプロシージャにしてしまえば、描画権利はすべてこちらに譲渡されます。
具体的な乗っ取り部分はこんな感じです:
// 親のウィンドウプロシージャ内
case WM_CREATE:
hButtonWnd = CreateWindow( _T("BUTTON"), NULL,
WS_CHILD | WS_VISIBLE | BS_OWNERDRAW,
0, 0, 100, 100, hWnd, (HMENU)ID_MI, hInst ,NULL);
// ボタンのオリジナルプロシージャを乗っ取り
DefaultButtonProc = (WNDPROC)GetWindowLong( hButtonWnd, GWL_WNDPROC );
// 自前のカスタムプロシージャを設定
// SetWindowLong( hButtonWnd, GWL_WNDPROC, (LONG)CustomButtonProc );
ボタンを生成してそのウィンドウハンドルを取得したら、次にGetWindowLong関数でデフォルトのウィンドウプロシージャを保持、さらにSetWindowLong関数で自前で用意したCustomButtonProcプロシージャ関数に切り替えます。この段階で、乗っ取り完了です。親のデフォルトプロシージャは絶対に必要なので、保持しておいて下さい。
CustomButtonProcプロシージャ関数にはボタンに対して何かした時にメッセージが飛んできますので、それに対応する処理を書きます。これはいつものプロシージャと全く同じでcase [メッセージ]〜break;を羅列すれば十分でしょう。
「ところで、ボタンプロシージャに入るメッセージはすべて処理しないとだめなの?」と思われるかもしれません。もちろんそんな事はありません。いつものウィンドウプロシージャの場合、自分に必要の無いメッセージは「DefWindowProc関数」に全部丸投げしていました。それと同様に、CustomButtonProc関数で処理しないメッセージは「CallWindowProc関数」という関数に丸投げすることができます:
LRESULT CallWindowProc(
WNDPROC lpPrevWndFunc,
HWND hWnd,
UINT Msg,
WPARAM wParam,
LPARAM lParam
);
第1引数のlpPrevWndFuncには丸投げするウィンドウプロシージャへのポインタを設定します。ボタンの場合、丸投げするウィンドウプロシージャはデフォルトボタンプロシージャに他ならず、それは先ほどDefaultButtonProcという変数に保持していました。絶対保持して欲しいと言ったのはこのためです。第2引数以下は丸投げする内容です。
以上を踏まえて描画部分だけを乗っ取った最も簡単なCustomButtonProc関数はこうなります:
LRESULT CALLBACK CustomButtonProc(HWND hWnd, UINT mes, WPARAM wParam, LPARAM lParam){
switch( mes ) {
case WM_ERASEBKGND:
// 背景描画を禁止する
return 1L;
case WM_PAINT:
PAINTSTRUCT ps;
HDC hDC = BeginPaint(hWnd, &ps);
// ここに描画処理を書く
EndPaint(hWnd, &ps);
break;
}
return CallWindowProc( DefaultButtonProc, hWnd, mes, wParam, lParam );
}
2つのメッセージを処理しています。
WM_ERASEBKGNDメッセージはボタンの背景を再描画する場合に呼び出されます。デフォルトではシステムが定めるブラシが使われ、長方形の形に塗りつぶされます。今はそれをされると困るので上のように空実装で処理を乗っ取り、1Lを返してCallWindowProc関数にも通さないように処理しています。1LにしているのはMSDNによると0Lを返さないで欲しいとあるためです。これで、ボタンの背景が無くなります。
WM_PAINTではボタン描画を行います。これはもうHDCを通して好きなように描いて下さい。丸でも四角でもテキストでも線でも、もちろんBMPだって描画できます。
上のプロシージャを通すと、ボタンの位置には何も表示されなくなります。透明状態です。でもその場所をマウスでクリックするとちゃんと反応してメッセージが飛びます。後はボタンの描画をすれば見た目カスタムボタンは完成します。でも実は、今の状態だとOSはボタンを「四角形だ」と判断してしまいます。丸や星型のボタンの絵を貼ってもマウスのクリックは四隅で反応してしまいます。丸いボタンを「丸だ」としてクリック反応させるには、ここからさらに「ウィンドウリージョン(領域)」を定める必要があります。
D 下準備その4:ウィンドウ領域の設定(リージョン設定)
何も考えずにボタンを作成すると、そのボタンは「四角である」と認識されます。でも丸や星型のボタンを作ろうと思った時にそれでは都合が良くありません。そこで登場するのが「リージョン」です。
すべてのウィンドウは「領域(リージョン)」を持っています。デフォルトはウィンドウ全体をきっちり覆う四角形なのですが、領域設定(リージョン設定)を行うとそれを好きな形に制限する事ができます。リージョンは領域ハンドル(リージョンハンドル)が管理しています。プログラマは用意されている幾つかの方法で領域を作成してそのハンドルを取得し、それをウィンドウに適用します。
ウィンドウ領域をウィンドウに設定するにはSetWindowRgn関数を用います:
SetWindowRgn関数 int SetWindowRgn(
HWND hWnd,
HRGN hRgn,
BOOL bRedraw
);
hWndには領域を設定したいウィンドウハンドルを指定します。今回の場合ここにはボタンのウィンドウハンドルが来ます。
hRgnには生成した領域ハンドルを渡します。
bRedrawはこの関数を設定した時に再描画を行うかどうかを指定します。通常は再描画をするのでtrueを渡せば問題ないでしょう。
先に注意申し上げておきますと、ここに設定したhRgnは以後システムが扱うので絶対に解放してはいけません。設定後ほっとけばシステムが解放してくれます。また領域設定は最初の1回行っておけば十分です。
領域ハンドル(HRGN)を得るための色々なAPIが用意されています。例えばCreateEllipticRgn関数を使うと円(楕円)の領域ハンドルを取得できます。長方形はCreateRectRgn関数、多角形はCreatePolygonRgn関数で生成します。基本的なプリミティブではなくて複雑な領域を作りたい場合はCombineRgn関数が大活躍してくれます。この辺りについては次章で説明致します。
各領域生成関数が成功すると、その形の領域ハンドルが返されます。後はそれを先のSetWindowRgn関数に渡せば、ウィンドウの有効領域がそれに制限されます。有効領域以外の部分へのアクションはすべて無視されます。領域外の部分をクリックしても反応しなくなるわけです。これで、丸いボタンを丸だと判定してくれます(^-^)
ボタンをオーナードローにして、ボタンプロシージャを乗っ取り、領域を設定する。後は領域にぴったりと合う絵を用意すれば、念願のカスタムボタンに到達します!
E 下準備5:用意する絵のお話
カスタムボタンは領域に合わせた自由な形にしたいのですが、キャンバスは長方形です。となると必然的に「透過処理」してはみ出た部分を描かない工夫が必要になります。
ここで、リージョンを使った事のある方ならば「リージョンを設定すればその形にウィンドウがくり貫かれるから、透過処理なんていらないでしょ?」と気付くかもしれません。実は私もそう思っていたのですが、どうもプロシージャを乗っ取って「内部」から描画を試みると、リージョンによる描画クリッピングが無効になるようです。つまり、いつもの長方形のキャンバスに長方形の絵が出現してしまいます。よって、透過処理は自前で実装しないといけません。
Windowsプログラムにおいてビットマップの透過貼り付けはすっかり定番になってしまいました。透過処理には「ソース画像」と「アルファ画像」の2枚のBMPを用意します。双方の画像の透明部分にはちょっとルールが必要になります。まずソース画像は透明にしたい部分を真っ黒にします。一方のアルファ画像の方は透明にしたい部分を真っ白にします。アルファ画像については白黒が反転していてもプログラム側で対処できるのですが、一応ここでは透明=白にしておきます。
ソース画像 アルファ画像
双方の画像をプログラムに読み込んでビットマップハンドル(HBITMAP)を作成しておきます。幾つか方法はありますが、LoadImage関数を用いたグローバル関数の作成例を以下に示します:
LoadImage関数によるビットマップハンドル取得関数 HBITMAP LoadBMP( HWND hWnd, TCHAR* file ) {
return (HBITMAP)LoadImage(
(HINSTANCE)(LONG64)GetWindowLong( hWnd, GWL_HINSTANCE ),
file,
IMAGE_BITMAP,
0, 0,
LR_LOADFROMFILE
);
}
こういうのは作っておくと何かと便利です(上コードはコピペで使えます)。
ボタンプロシージャ内にWM_PAINTが飛び込んできたら、BMPの描画を行います。すでにHBITMAP型のソース画像ハンドル(hSrc)とアルファ画像ハンドル(hAlpha)があるとして、WM_PAINT内は例えば次のようになります:
// ボタンプロシージャ内
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hDC = BeginPaint(hWnd, &ps);
// アルファマスクを背景に描画
HDC hdc_mem = CreateCompatibleDC( hDC );
SelectObject( hdc_mem, hAlpha );
BitBlt( hDC, 0, 0, width_, height_, hdc_mem, 0, 0, SRCAND );
// ボタンを描画
SelectObject( hdc_mem, hSrc );
}
BitBlt( hDC, 0, 0, width_, height_, hdc_mem, 0, 0, SRCPAINT );
DeleteDC( hdc_mem );
EndPaint(hWnd, &ps);
break;
}
最初にデバイスコンテキストハンドル(hDC)とそのコンパチブルデバイスコンテキスト(hdc_mem)を作成します。次にhdc_memにアルファマスクを適用し(SelectObject)、それをhDCにブリットします。ブリットのフラグはSRCANDです。元の色(=背景)とAND(論理積)で合成するフラグですね。今アルファ画像の透明色は白(0xffffff)です。1とのANDは元の色が反映されます。つまり、アルファの透明部分には背景の色がそのまま反映されます。一方黒い部分はビットが0なので、何とANDしても0になります。よってこの処理後、背景には真っ黒な影のようにボタンの形が描かれる事になります。
綺麗にボタンの形にだけくりぬかれています。
続けてボタンのソース画像を背景に描画します。この時のフラグはSRCPAINTです。SRCPAINTはソース画像と背景をOR(論理和)で合成するフラグです。今ソースの透明部分は黒(0)です。0と何かを足し算するということは透明部分は背景がそのまま合成されます。一方非透明部分はソース画像(ボタンの画像)になっています。上のウィンドウのように、今現在背景のボタンが描かれる予定部分は真っ黒です。ですからORを取るとその部分はボタンの画像が反映されます。これにより、めでたくボタン画像だけがその形でくり貫かれて背景(親ウィンドウ)に表示される事になります。
見た目汚いのは私のボタン加工の怠惰のせいです。すいません…
BitBltに与えるフラグは、サイトによって微妙に異なります。これは透過色の定義が異なるだけでして、やっている事はみんな同じです。統一は無さそうなので、こんがらがらないよう注意して下さい。
F お話はここからが本番:カスタムボタンクラスへの道
「あ〜できたぁ」と気を抜いてはいけません!今やっと究極カスタムボタンへの道のスタート地点に来たんですから(驚)。本番はここからなんです。疲れた方はコーヒーブレイクをどうぞ。
さて、A〜Eまでを踏まえるとカスタムボタンを完全に作成できます。これはぜひともクラス化してライブラリとして使いたいわけです。しかし、乗り越えなければならない壁があります。大きな壁は「プロシージャ関数が静的である」という制約、言い換えればグローバル関数でなければならないという制約です。パッケージ化を基本とするクラスにとって、これは最大の邪魔者です。例えばボタンが2つあって、それぞれ異なる形状のボタンにしたい場合、現状だとカスタムボタンプロシージャを2つ必要とするわけです。でもクラスでしたいのはきっと、プロシージャ関数が仮想関数になっていて、派生ボタンで振る舞いを変えられるという仕様ではないでしょうか?どうやってそれを実現するか?それがこの節の最大のポイントになります。
カスタムボタンクラス(CustomButtonクラス)をまずは新設します。このクラスに1つのstatic宣言されたグローバルボタンプロシージャメソッドと仮想関数として定義されるローカルボタンプロシージャを定義します:
CustomButtonクラス class CustomButton {
protected:
static LRESULT CALLBACK globalButtonProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
virtual LRESULT localButtonProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
};
CustomButtonボタンオブジェクトを生成した時、デフォルトボタンプロシージャに変わって差し込まれるのはglobalButtonProcクラスメソッドです。これは静的なので合法です。CustomButtonによるあらゆるボタンのメッセージは、いったんこのグローバルなクラスメソッドにすべて集約されるわけです。
globalButtonProcメソッドの中でメッセージの処理はしません。それはローカルプロシージャの役目です。globalButtonProcメソッド内では現在生成されている各ボタンが持つローカルボタンプロシージャへ処理を投げる仕組みを整えます:
CustomButton::GlobalButtonProcメソッド LRESULT CALLBACK CustomButton::globalButtonProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {
// 引数のボタンウィンドウハンドルから
// 登録されているボタンオブジェクトを取得
CustomButton *p = CustomButton::getObjPtr( hWnd );
// プロシージャ呼び出し
if ( p )
return p->localButtonProc( hWnd, msg, wParam, lParam );
return 0L;
};
引数のウィンドウハンドルは1つのボタンに固有のハンドルです。よって、ハッシュテーブルを作成すれば、それを頼りにボタンオブジェクトを引っ張って来る事ができます。その役目をするのがgetObjPtrメソッドです。getObjPtrメソッドはCustomButtonクラスが持つクラスメソッドとして定義します。
オブジェクトポインタがあれば、それを通してローカルボタンプロシージャを呼び出します。ポインタを通すので多態性(=ポリモーフィズム)が使えます。こうすると、CustomButtonクラスの派生クラスで再定義したlocalButtonProc関数(仮想関数)に飛ぶわけです。
静的なgetObjPtrメソッドは例えば次のようにバイナリツリーを使って実装できます:
CustomButton::getObjPtrメソッド CustomButton* CustomButton::getObjPtr( HWND hWnd )
map< HWND, CustomButton* >::iterator it = CustomButton::btnHash.find( hWnd );
if( it != CustomButton::btnHash.end() )
return it->second;
return NULL;
};
CustomButton::btnHashはstl::map型のハッシュ変数です。これも静的である必要があります。ここに登録されているボタンがあれば、そのポインタを返す実装になっています。ソース自体はmap::findの典型的な使い方です。
CustomButton::btnHashにユーザが一々ボタンを登録するのは正直面倒ですし、デフォルトボタンに無い動作ですからちょっといただけない。そこで、クラス自体がボタンを生成して登録する仕様にしてしまいます。
生成を担当するcreateメソッド(静的メソッド)を新設します:
CustomButton::createメソッド void CustomButton::create( HWND hParentWnd, CustomButton** out ) {
*out = new CustomButton( hParentWnd );
CustomButton::regist( out );
return sp;
};
コンストラクタに親ウィンドウハンドルを渡しています。コンストラクタ内ではその情報からボタンを生成します。newで動的に生成したカスタムボタンオブジェクトは、直ぐにregistクラスメソッドを通してハッシュに登録されます:
CustomButton::registメソッド void CustomButton::regist( CustomButton *pBtn )
// ボタンのウィンドウハンドル取得
HWND hWnd = pBtn->getWndHandle();
// バイナリツリーに登録
CustomButton::btnHash.insert( pair< HWND, sp<CustomButton*> >( hWnd, pBtn ) );
};
std::mapの典型的な登録方法を使っているだけです。
これで生成から登録、プロシージャの呼び出しまでが完了しました。残るは「消去」です。オブジェクトをnewで生成しているので、どこかで必ずdeleteする必要があります。この作業もユーザにはさせません。ボタンを確実に使用しなくなるタイミングを考えてみると、「親ウィンドウがさよならする時」つまりWM_DESTROYが呼ばれる時が適切かなと思います。このタイミングを捕まえて、ボタンオブジェクトを消し去ることにしましょう。
すべてのボタンのWM_DESTROYを統一的に捕まえられるのは、唯一globalButtonProcメソッド内だけです:
CustomButton::GlobalButtonProcメソッド LRESULT CALLBACK CustomButton::GlobalButtonProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
// 登録されているボタンオブジェクトをウィンドウハンドルから取得
CustomButton *p = CustomButton::getObjPtr( hWnd );
// プロシージャ呼び出し
if ( p ) {
LRESULT res = p->localButtonProc( hWnd, msg, wParam, lParam );
// もしWM_DESTROYを捕まえたらボタンを消去する
if ( msg == WM_DESTROY ) {
delete it->second;
CustomButton::btnHash.erase( it );
}
return res;
}
return 0L;
};
メモリからdeleteで削除して、忘れずにハッシュからも削除しておきます。これでカスタムボタンクラスの外堀が出来上がりました!
後は瑣末的なメソッドを用意することになります。例えばボタンの絵(HBITMAP)を登録するsetPictureメソッドや、ボタンの位置を設定するsetPosメソッド、領域を設定するsetRgnメソッドなどです。
LocalButtonProcメソッドは色々な書き方があるのですが、この章のボタン絵を表示しているプロシージャを例とするなら、こんな感じです:
CustomButton::LocalButtonProcメソッドの例 /// ローカルプロシージャ
LRESULT CustomButton::localProc( HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam ) {
switch ( msg ) {
case WM_LBUTTONDOWN: {
isPush_ = true;
requestRedraw();
break;
}
case WM_LBUTTONUP: {
isPush_ = false;
requestRedraw();
break;
}
case WM_SETFOCUS: {
isFocus_ = true;
requestRedraw();
break;
}
case WM_KILLFOCUS: {
isFocus_ = false;
requestRedraw();
break;
}
case WM_ERASEBKGND: {
// 背景描画を禁止する
return 1L;
}
case WM_PAINT: {
PAINTSTRUCT ps;
HDC hDC = BeginPaint( hWnd_, &ps );
// マスクを描画
HDC hdc_mem = CreateCompatibleDC( hDC );
SelectObject( hdc_mem, alpha_ );
BitBlt( hDC, 0, 0, width_, height_, hdc_mem, 0, 0, SRCAND);
// 押し下げ情報に合わせてボタンを描画
if ( isPush_ ) {
SelectObject( hdc_mem, push_ );
}
else {
SelectObject( hdc_mem, normal_ );
}
BitBlt( hDC, 0, 0, width_, height_, hdc_mem, 0, 0, SRCPAINT);
DeleteDC( hdc_mem );
EndPaint( hWnd_, &ps );
return 0L;
}
}
return CallWindowProc( GetDefProc(), hWnd, msg, wParam, lParam );
}
クリックチェックとフォーカスチェック、そして描画部分では通常画像と押し下げ時の画像をisPush_を見て切り替えています。
ちなみに、このボタンを表示させるに当たっての親ウィンドウの作業はこれだけです:
親ウィンドウプロシージャの作業 case WM_CREATE:
{
// カスタムボタン生成
CustomButton::create( hWnd, &CB1 );
CB1->setPicture(
LoadBMP( hWnd, _T("Button_norm.bmp") ),
LoadBMP( hWnd, _T("Button_push.bmp") ),
LoadBMP( hWnd, _T("Button_a.bmp") )
);
CB1->setPos( 20, 20 );
break;
}
生成して絵を登録して、位置を決めて終わり。これはデフォルトボタンの初期化と実質ほとんど同じです。もちろん、WM_PAINTには背景の絵以外ボタンについての作業は1文字もありません。消去作業もありません。これが究極です(^-^)v
このカスタムボタンクラスの完全版はサンプルプログラムとして公開致します。公開ソースは上で紹介しているものをより洗練させて、親となるCustomComponentクラスを頭にしています。staticメソッドはすべてCustomComponentクラスにまとめ、CustomButtonクラスと分離させました。これにより他のコンポーネントもすぐにクラス化できます。またリージョンは「自動的に」作成されます。クラスの使い方は決して難しくありませんし拡張も容易だと思いますので、どうぞチャレンジしてみて下さい。
カスタムボタンの仕組みがわかり、そのクラス化もできるようになると、エディットウィンドウやスクロールバーなど他のコンポーネントも同様の考え方でカスタム描画できるようになります。それを丁寧に作りこんでいけば、スキンで完全に覆われた独自のデザインのアプリケーションの生成も可能です。メディアプレイヤーのスキンのような感じですね。これが実現できれば、もう他のアプリケーションと見栄えだけで差別化がはかれます。そうなれば、楽しいですよね〜〜。