2010年02月19日
ムービーファイルの読み込みについて考えてみた その5 modo 401 SP2
プログラムにバグが見つかりました。自分のコンピュータではmodoSP3になってバグが顕在化しましたが、SP2でも偶然動いていただけであることがわかりました。
バグについてはすでに修正しました。詳しくはこちらをごらん下さい。
前回はダイアログボックスを出すDialogBoxファンクションに渡すいろんなパラメータについて見てきた。今回は前回の最後で尻切れになってしまったダイアログボックスにポインタを記録する話から。
ウィンドウやダイアログボックスが生成されると、それぞれにスタイルやら操作に反応するプロシージャのアドレスとかを格納するエリアが個別に確保される。そこに値をセットしたり読み込んだりするためのファンクションがSetWindowLongとGetWindowLongだ。どのウィンドウに対して操作するかはウィンドウハンドルで指示して、どのパラメータにアクセスするかはパラメータ格納領域の先頭からの相対位置をオフセット値で指示するようになっている。
こっちが値をセットする方で、
LONG SetWindowLong(
HWND hWnd, // ウィンドウのハンドル
int nIndex, // 設定する値のオフセット
LONG dwNewLong // 新しい値
);
こっちが値を読み出す方ね。
LONG GetWindowLong(
HWND hWnd, // ウィンドウのハンドル
int nIndex // 取得する値のオフセット
);
そしてリファレンスを見るとダイアログボックス向けにユーザーが自由にデータを書き込めるエリアがある事があわる。それがDWL_USERのところで、
DWL_USER:ハンドルやポインタなどの、アプリケーション固有の拡張情報を取得します。
WinUser.hに
#define DWL_USER 8
と定義されている。ダイアログのこの領域を使ったクラスを使った複数ダイアログプロシージャの仕組みについて下に図にしてみた(クリックすれば大きくなるよ)。
前回も書いた通りインスタンス化したオブジェクトのメソッドは内部的に配列で管理されていてそのアドレスを取得出来ない。だからダイアログボックスを生成するDialogBoxファンクションにアドレスを渡す事ができない(上の図で「オブジェクト」と書かれた中に入っているダイアログプロシージャ)。それに対してクラスの静的メソッドはアドレスが取得出来るので、DialogBoxファンクションにダイアログプロシージャを登録する事は出来る(図の「クラス」と書いてあるところのダイアログプロシージャ)。しかし静的メソッドはクラスの静的じゃないメンバーに直接アクセスする事はできない。そこで前回は静的なメソッドをダイアログボックスに登録し、静的なメンバー変数にクラスをインスタンス化したオブジェクトのアドレスを記録しておいて、ダイアログプロシージャがダイアログイベントで呼び出された時にその静的なメンバー変数に記録されているオブジェクトを経由して、本来呼び出したかったオブジェクトの中のダイアログプロシージャを呼び出すようにする事にしたわけだ。それが前回までの話。でもこの場合、1つのクラスから生成した複数のダイアログボックスは全て1つの共通の静的なメソッドをダイアログプロシージャとして持つ事になってしまう(図のクラスのダイアログプロシージャは2つのダイアログボックスで共有されている)。
そこで、複数のダイアログでプロシージャは共通のまま、そのプロシージャを呼び出したダイアログにあわせて呼び出すべきプロシージャを切り替えて実行する仕組みを共通のプロシージャに持たせる事にしたわけだ。
そこで登場するのが上で紹介したGetWindowLongとSetWindowLongファンクションとダイアログごとに自由に使えるDWL_USER領域だ。
個々のダイアログのDWL_USERにそれを処理するためのプロシージャを持ったオブジェクトのアドレスを登録する手順は、
- クラスをインスタンス化してオブジェクトを生成する
- オブジェクトのアドレスをthisなどで得て、それを静的メンバのm_hWndに代入する
- 各ダイアログボックス共通の静的メソッドを引数にして、DialogBoxファンクションでダイアログボックスを生成する
- ダイアログボックスが表示される前にダイアログボックスに登録されているプロシージャが「WM_INITDIALOG」というメッセージと、プロシージャを呼び出したダイアログのインスタンスハンドルと共に呼び出される。ダイアログプロシージャでこれを検知したら、m_hWndに登録されているオブジェクトのアドレスをSetWindowLongでインスタンスハンドルが示すダイアログボックスのDWL_USER領域に書き込む。
ダイアログボックスを生成する前にm_hWndに代入するオブジェクトを変えて上の手順を繰り返せば、各ダイアログボックスには異なるプロシージャを持つオブジェクトが登録される事になる。
次にダイアログボックスに対して何らか操作をした時、イベントが発生して、それを処理するために共通のプロシージャが呼び出されるけど、その動作は次のようになる。
- イベントが発生すると、そのイベントに対応するメッセージやパラメータと共にイベントが発生したダイアログのインスタンスハンドルが共通のプロシージャに渡される。.
- インスタンスハンドルを使ってGetWindowLongでDWL_USERから、オブジェクトのアドレスを取得して変数pに代入する。
- 変数pからそこに登録されているオブジェクト経由で、ダイアログプロシージャを呼び出す。
これで、ダイアログでイベントが発生するたびに変数pがDWL_USERに登録された値で再設定されて、pが示すオブジェクトが持つダイアログプロシージャが呼び出されて実行されるようになる。
以下がそのコードだ。ついでにウィンドウが掃きされた時に送られてくるメッセージ(WM_DESTROY)で、これ以上プロシージャが反応しないようにDWL_USERに0を書き込んでいる。
BOOL CALLBACK CAviLoader::dialogProc(HWND hwnd,UINT uMsg, WPARAM wParam, LPARAM lParam )
{
CAviLoader *p;
if (uMsg == WM_INITDIALOG){
SetWindowLong(hwnd,DWL_USER,(DWORD)m_hWnd);
m_hWnd = NULL;
}
p=(CAviLoader*)GetWindowLong(hwnd,DWL_USER);
if(uMsg==WM_DESTROY){
SetWindowLong(hwnd,DWL_USER,0);
}
if (p) return p->dlgproc(hwnd, uMsg, wParam, lParam);
return FALSE;
}
こんなクラスをベースクラスとしてオリジナルのダイアログボックスのためのクラスを派生させれば、オブジェクト指向っぽいプログラムになるわけだね。
さて次に、ようやくホンモノのダイアログプロシージャの話だ。今回リソースエディタで作成したダイアログボックスは3つのボタンが並んでいるだけのものだ。これが押されると、ダイアログプロシージャが呼び出される。
BOOL CALLBACK DialogProc(
HWND hwndDlg, // ダイアログボックスのハンドル
UINT uMsg, // メッセージ
WPARAM wParam, // 最初のメッセージパラメータ
LPARAM lParam // 2 番目のメッセージパラメータ
);
ボタンが押された時に渡されるパラメータの場合、hwndDlg:どこのダイアログが呼んだか、uMsg:WM_COMMAND(メニューが選択されたり、ボタンが押されたりアクセラレータキーが押されたりしたというメッセージ)、wParam<下位ワード>:ボタンのID番号を見れば、どこのダイアログボックスのどのボタンが押されたかがわかるので、それを識別して、ボタンが押された時の処理を書けばいい。
ちょうどこんな感じだ。
BOOL CALLBACK DialogProc(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam)
{
switch(uMsg){
case WM_COMMAND:
switch(LOWORD(wParam)){
case ボタンAのID:
ボタンAが押された時の処理
break;
case ボタンBのID:
ボタンBが押された時の処理
break;
:
}
}
return FALSE;
}
そしてこのボタンのIDはリソースを定義した時にヘッダファイルに書き込んだあれだ。
#define IDC_BACK 100
#define IDC_CAPTURE 101
#define IDC_FORWARD 102
これらが各ボタンに対応している。
今回はボタンによってキャプチャーするムービーのコマを1コマずつ進めたり戻したりコマを決定したりすることが目的だ。そしてキャプチャするコマの番号はメンバー変数m_frame_noで、最初に0で初期化されていて、また開いたムービーの総コマ数はm_total_timeに入れてあるので、あとは0~m_total_time-1の間でm_frame_noの値を変化させて、directshowにそのコマを表示させるようにして、コマが決まったら、表示されているコマをキャプチャしてmodoに渡すようにすればいい。
そこでこんな形に落ち着いた。
BOOL CALLBACK CAviLoader::dlgproc(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam)
{
switch(uMsg){
case WM_INITDIALOG:
getframe();
break;
case WM_COMMAND:
switch(LOWORD(wParam)){
case IDC_BACK:
if(m_frame_no>0) --m_frame_no;
getframe();
break;
case IDC_FORWARD:
if(m_frame_no<m_total_time-1) ++m_frame_no;
getframe();
break;
case IDC_CAPTURE:
EndDialog(hwnd,-1);
return TRUE;
}
}
return FALSE;
}
getframe( )は現在のムービーの表示をm_frame_noの変数が示す番号のコマに設定するファンクションだ。
WM_INITDIALOGは前述した通りダイアログが表示される直前に送られてくるメッセージで、この時にgetframe( )を呼び出すことで、ムービーを表示するウィンドウを出して0コマ目を表示させている。これが無いと何かボタンが押されるまでgetframe( )が実行されないので、ムービーが表示されなくなってしまう。
EndDialog(hwnd,-1);はダイアログボックスを閉じるファンクションだ。キャプションボタンが押されたらダイアログボックスを閉じるだけしかやっていない。モーダルダイアログボックスが閉じると、それを呼び出したところに制御が戻ってその時点で表示されているフレームをメモリに取り込んでmodoに渡すようにしてある。パラメータの−1はDialogBoxファンクションの戻り値になるけど今回は特に使ってない。
長くなったので続きはまた来週。