そして時は動き出す・・・もう「応答なし」でフリーズさせないッ!

応答なし・・・?

ども。 エイタです!

時間のかかる処理を実行中にアプリケーションが「応答なし」になり、いつ処理が終わるかも知れず、ただただ画面を見つめているときってありますよね! 処理がどこまで進んでいるかわかるように進捗状況を画面に表示するようにしたけど、それも止まってしまった、という元も子もない事態に陥ってアプリケーション同様にフリーズしている姿が目に浮かびます。

そんな時には「奥の手」、Yield 関数を使うと便利ですよ!


「応答なし」とは?

Windows 7 が出始めたころからか、アプリケーションの CPU 使用率が高い状態が一定時間続いている場合に、アプリケーションのタイトルバーに(応答なし)と表示されて画面がフリーズしてしまう、といった事象に悩まされている方が多かったようです。 これは決して PowerBuilder の不具合や仕様変更ではなく、OS の仕様によるものです。

この機能のおかげで以前のバージョンのような、動いてるのか止まっているのか判断できないといったことにならなくなったわけですが、過去バージョンで作成した PowerBuilder アプリケーションを最新バージョンにマイグレーションしたタイミングで OS のバージョンも上げたことによって画面が止まり、驚かれたという方も少なくないでしょう。

優先度の低い画面描画処理をストップし、CPU をバックグラウンドの処理に割り当てることで処理の時間を短くしようとしているわけですが、画面が止まっちゃったら困ることもありますよね。 せっかく処理の進捗を動的に楽しく画面に表示していたのに、これでは処理が終わるまでユーザーに退屈な時間を過ごさせることになってしまいます。


Yield 関数とは?

そんな時に使えるのが Yield 関数。この関数は OS のメッセージキューをチェックし、メッセージがあった場合にはそのメッセージをキューから取り出します。 つまり、溜まっていた処理を省略せずにひとつひとつ実行していくというイメージです。 画面描画処理なんかは CPU の使用率などに応じて飛ばされてしまうことが多いのですが、これを懇切丁寧に描画してあげることができるようになります。

Yield 関数の構文

boolean Yield ( )

引数は無し。 戻り値は Boolean 型で、キューからメッセージを取り出した場合には TRUE を返し、メッセージがない場合には FALSE を返します。 Yield 関数をループ処理のなかで呼び出すと、ループ内の表示処理等の処理や他のアプリケーションの処理を発生させてから次の処理に移ります。

なお、Yield 関数を利用する上での注意として下記の点が挙げられます。

  • 処理時間が増える
    • 省略されるはずの処理も漏れなく行われるようになるため、そのぶん処理に時間が掛かるようになります。
  • 処理中に別の処理が割り込まれる
    • 通常、関数やイベントなど、ひとつの処理中には他の処理が割り込まれない動作になっていますが、ボタンクリックなどが可能になり処理が割り込まれてしまいます。 インスタンス変数など、共通の変数やオブジェクトなどを利用している場合、別の処理で変数の値が書き換えられたりすると想定しない動作となる場合があります。

「応答なし」の回避方法

さっそく「応答なし」を制御してみましょう。 こんな画面で試してみます。

サンプルプログラム

処理中はこの子犬ちゃんが飛び回ります

[実行] ボタンをクリックすると、下記のような時間のかかるループ処理が行われます。 時間が掛かるためプログレスバーで進捗状況を表示しつつ、暇にさせないためウィンドウ上にピクチャコントロールを配置してパラパラマンガのように複数 (12 枚) の画像を切り替えてアニメーション的に表示する処理を追加しています。

[実行] ボタン Clicked イベント

long ll_loop, ll_count
integer li_res

ll_count = 5000

FOR ll_loop = 1 TO ll_count
    // ステータスを表示
    st_status.text = "#" + String(ll_loop) + " の処理を実行中..."
    
    // 重たい処理
    li_res = wf_heavy(ll_loop)
    st_status.text += " [" + String(li_res) + "]"
    
    // プログレスバーを移動
    hpb_1.Position = ll_loop / ll_count * 100
    
    // タイトルを設定
    parent.title = "進捗状況: " + String(ll_loop / ll_count * 100, "##0.00") + "%"
    
    // 犬を遊ばせる(画像を切り替え)
    p_1.picturename = ".\img\inu-" + String(Mod(int(ll_loop / 10), 12), "00") + ".png"
NEXT

// 完了処理
st_status.text = "完了"
MessageBox("Info", "処理が完了しました。")

// プログレスバーを初期化
hpb_1.Position = 0

// タイトルを戻す
parent.title = "サンプル処理"

// 画像を戻す
p_1.picturename = ".\img\inu-00.png"

このまま実行すると・・・。

応答なし

世界(ザ・ワールド)」ッ!!

案の定、処理を開始して数秒で「応答なし」になり画面がフリーズしてしまいました。 元気に飛び跳ねていた子犬 (子犬です) も動きを止めています。 心なしか悲しそうにも見えます。

そこで Yield の登場です。 ループ処理の先頭に Yield の呼び出しを追加してみましょう。

[実行] ボタン Clicked イベント (抜粋)


・・・(省略)・・・

FOR ll_loop = 1 TO ll_count

    Yield() // 処理を描画

    // ステータスを表示
    st_status.text = "#" + String(ll_loop) + " の処理を実行中..."
    
    // 重たい処理
    li_res = wf_heavy(ll_loop)
    st_status.text += " [" + String(li_res) + "]"

・・・(省略)・・・

どうかな?

完遂

疲れを知らない無邪気な子犬でした

最後までフリーズせずに処理を完遂させることができました!

しかしちょっと待ってください。 Yield 関数を追加したことで処理中の割り込みができるようになり、今のままでは処理の最中にユーザーによる様々な操作が受け付けられる状態になっています。 たとえば、処理の途中で実行ボタンをもう一度クリックしてみたり、画面を閉じてみたりと好き放題、やりたい放題です。

そんなことをされてしまうと処理の整合性が取れなくなったり、処理が完了する前に画面が閉じられて中途半端にデータが更新されたりしてしまいます。 ・・・それは困る。


画面の制御

そんなことにならないように、ボタンをクリックできなくしたり、画面を閉じられないようにしたりといった制御をする処理を追加してあげる必要があります。

処理を記述したイベントが発生しないように各コントロールの Enabled プロパティを FALSE に設定してあげましょう。 ウィンドウに配置されたコントロールが多い場合は・・・まぁ大変ですね。 その場合は別途、一括でコントロールを制御するような処理を作ったほうがいいと思います。

今回はボタン数個なので直接処理に追加します。 処理の先頭で、処理中にクリックされたくないボタンを使用不可にし、処理が終わってからクリックできるように戻してあげます。

[実行] ボタン Clicked イベント (抜粋)

long ll_loop, ll_count
integer li_res

// ボタン制御
cb_run.enabled = FALSE
cb_exit.enabled = FALSE

ll_count = 5000

FOR ll_loop = 1 TO ll_count

    Yield() // 処理を描画

    // ステータスを表示
    st_status.text = "#" + String(ll_loop) + " の処理を実行中..."

・・・(省略)・・・

// ボタン制御
cb_run.enabled = TRUE
cb_exit.enabled = TRUE

更にウィンドウの [×] ボタンを押されたときに画面を閉じないように制御します。 まず、処理中であることを表すフラグ (Boolean 型) をインスタンス変数に追加し・・・

インスタンス変数の宣言

boolean ib_processing = FALSE

Window の CloseQuery イベントで、処理中 (ib_processing = TRUE) の場合は画面のクローズをキャンセル (RETURN 1) しましょう。

CloseQuery イベント

IF ib_processing THEN
    MessageBox("Warning", "処理中のため画面を閉じることはできません。", Exclamation!)
    // クローズをキャンセル
    RETURN 1
END IF

CloseQuery イベントは画面が閉じられようとしたときに発生するイベントです。 [×] ボタンだけでなく、Close 関数により画面が閉じられる場合にも発生します。 動作を確認するため、処理中に [×] ボタンをクリック!

画面のクローズをキャンセル

メッセージボックスが表示されたあと、画面は閉じられずに処理を続行します。 これでひとまず安心ですね。


あえての割り込み処理

しかし、処理中にあえてボタンを押したい時もありますよね。例えば処理を「中断」したい場合などです。 そこで、Yield 関数によりボタンがクリックできるようになることを利用し、意図的な割り込み処理を追加してみましょう。

画面に [中断] ボタンを追加し、Clicked イベントに下記の処理を追加します。

[中断] ボタン Clicked イベント

// 処理を中断
ib_processing = FALSE

そして、このインスタンス変数が処理中に FALSE に切り替わった場合にループを抜ける処理を追加します。

【最終形】[実行] ボタン Clicked イベント

long ll_loop, ll_count
integer li_res

// 処理開始
ib_processing = TRUE

// ボタン制御
cb_run.enabled = FALSE
cb_abort.enabled = TRUE
cb_close.enabled = FALSE

ll_count = 5000

FOR ll_loop = 1 TO ll_count

    Yield() // 処理を描画
    
    // 中断されたら処理を中止
    IF NOT ib_processing THEN
        EXIT
    END IF
    
    // ステータスを表示
    st_status.text = "#" + String(ll_loop) + " の処理を実行中..."
    
    // 重たい処理
    li_res = wf_heavy(ll_loop)
    st_status.text += " [" + String(li_res) + "]"
    
    // プログレスバーを移動
    hpb_1.Position = ll_loop / ll_count * 100
    
    // タイトルを設定
    parent.title = "進捗状況: " + String(ll_loop / ll_count * 100, "##0.00") + "%"
    
    // 犬を遊ばせる(画像を切り替え)
    p_1.picturename = ".\img\inu-" + string(mod(int(ll_loop / 10), 12), "00") + ".png"

NEXT

// 完了処理
IF ib_processing THEN
    st_status.text = "完了"
    MessageBox("Info", "処理が完了しました。")
ELSE
    st_status.text = "中断"
    MessageBox("Info", "処理を中断しました。")
END IF

// プログレスバーを初期化
hpb_1.Position = 0

// タイトルを戻す
parent.title = "サンプル処理"

// 画像を戻す
p_1.picturename = ".\img\inu-00.png"

// 処理終了
ib_processing = FALSE

// ボタン制御
cb_run.enabled = TRUE
cb_abort.enabled = FALSE
cb_close.enabled = TRUE

処理中に [中断] ボタンをクリックしてみます。 うまく動くか・・・?

処理を中断

急に中断されて驚いちゃった?

思い通りに処理を途中で止めることができました。 Yield により割り込み処理が可能になった故の芸当です。


まとめ

前述の通り、Yield 関数は間違った使い方をすると致命的なデータの不整合を起こすなどの危険が潜んでいるため、おおっぴらにはオススメできない類の関数です。 それでも、この関数でしか実現できない処理もあるので、利用する場合は十分に注意をして活用してみましょう!

以上、エイタでした!

テクニカルブログ 一覧を見る
PowerBuilder マイグレーション