QMAの自習プログラムを作る その3

問題文をゆっくり表示する

クイズと言えば、問題文の最初の方だけでどういう問題かを予想し、素早く解答する力を試すという面もあります (そしてまんまとひっかかるのも1つの楽しみ)。 この楽しみを実現するには、問題を一度に提示せず、読み上げるか(テレビのクイズ番組のような)、 あるいは文を徐々に表示させる(QMAのような)といった工夫が必要です。

というわけで次の目標設定は「時間の経過と共に問題文を前からだんだん表示させる」です。

文字の描画は DrawText 関数で行っていますが、この第 3 引数に描画する文字数を指定できます。 今までは問題文の文字数を指定して全部一度に表示させていましたが、 ここの文字数を時間の経過と共に増やしていって、次第に全体が見えるようにします。

時間と共に値を増やすにはどうしたらいいでしょうか? これにはタイマーを使用します。 タイマーは一定間隔ごとにプログラムに対して通知をしてくれます。 使用する API は SetTimer 関数です。 引数を次に示します。

引数意味
HWND hWnd タイマに関連付けられるウィンドウハンドル
UINT_PTR nIDEvent タイマを識別するID
UINT uElapse タイムアウト値(ミリ秒単位で指定)
TIMERPROC lpTimerFunc タイムアウト時に処理を行うプロシージャ(関数ポインタ)

タイマを識別するIDはプログラマが自由に設定します。 すでに有効なタイマーIDに対して再びSetTimerを呼び出すと、 前の設定は無効になり上書きされます。 第 4 引数には関数ポインタを指定するとありますが、NULL も可能です。 この場合、WM_TIMER メッセージが第 1 引数で指定したウィンドウ宛に送られます。

表示する文字数のカウント用に VisibleChars という int 型の変数を新しく作って、 タイムアウトの度にインクリメントすることにします。 まずは SetupQuestion 関数で VisibleChars 変数を初期化し、タイマーをセットします。

    void SetupQuestion( HWND window ) {
        VisibleChars = 0;
        SetTimer( window, ID_TIMER, 50, NULL );
        random_shuffle( IndexShuffler.begin(), IndexShuffler.end() );
        ShowWindow( GetDlgItem( window, ID_OBUTTON ), SW_HIDE );
        ShowWindow( GetDlgItem( window, ID_XBUTTON ), SW_HIDE );

ウィンドウプロシージャ内で WM_TIMER に対する処理を追加します。

        case WM_TIMER:
            VisibleChars++;
            if( VisibleChars == Questions[QuestionNumber].TextLength )
                KillTimer( window, ID_TIMER );
            InvalidateRect( window, &textrect, FALSE );
            break;

VisibleChars が最大値(=問題文全体の文字数)に達したら、KillTimer 関数でタイマーを殺します。 文字数を増やすごとに問題文を再描画させるために、InvalidateRect 関数を呼びだします。 textrect は RECT 構造体で、WM_PAINT で DrawText に渡すものと同じです。

DrawText の文字数を VisibleChars に変更します。

        case WM_PAINT: // 問題文を描画する
            {
                PAINTSTRUCT ps;
                HDC hdc;
                hdc = BeginPaint( window, &ps );
                DrawText( hdc, Questions[QuestionNumber].Text, VisibleChars, &textrect, DT_LEFT|DT_WORDBREAK );
                EndPaint( window, &ps );
            }

実行してみましょう。

意外とすぐにできました。 私のマシンではさほど気になりませんでしたが、 このコードでは 1 文字 1 文字増える度に全体を再描画しているので、 問題文がチカチカして見える人もいるかもしれません。 DrawText 関数が文字を描く前に一度背景色で消去するためにこのようなことが起こります。 BeginPaint の下に次のように SetBkMode 関数を入れるとチカチカが防げます。

        case WM_PAINT: // 問題文を描画する
            {
                PAINTSTRUCT ps;
                HDC hdc;
                hdc = BeginPaint( window, &ps );
                SetBkMode( hdc, TRANSPARENT );
                DrawText( hdc, Questions[QuestionNumber].Text, VisibleChars, &textrect, DT_LEFT|DT_WORDBREAK );
                EndPaint( window, &ps );
            }

SetBkMode 関数でデバイスコンテキストに対し TRANSPARENT を指定すると、 文字の描画の前に背景色で消去しなくなります。 つまり背景を残したまま文字だけを描画します。 文字が少しずつ移動するようにアニメーションしたり別の文字を上書きしたりする場合は 古い文字を消去しなければいけないので、この指定はマズいのですが、 今回は全く同じ位置に同じ文字を重ねて描くので問題ありません。 むしろ背景で塗り潰す手間が省けるのと、背景で塗り潰すことによるチカチカが無くなるので好都合です。

この記事を書き終えたところでちょっと調べたら、 WM_TIMER は精度が悪いという噂が。 私の環境では少なくとも 10ms 程度の精度は出ているみたいですが、果たして…?。 上のコードで指定しているのは 50ms なので多くの場合問題ないとは思います。 もっとシビアな状況で使うならばマルチメディアタイマー(timeSetEvent 関数)を使った方がいいのかもしれません。 (ここではその使い方は解説しません。というか私も知りません)

時間を計る

問題の解答に制限時間を設けましょう。QMA と同じ 20 秒です。 解答せずに時間が経過したら、「時間切れ」の表示と共に選択肢を選べないようにします。

(書き途中)


作成日 2011/02/24 ... 最終更新日 2011/02/25
by zeroichi

QMAは株式会社コナミデジタルエンタテインメントの登録商標です