それわVBA案件ですね

エクセルVBAネタを書いています

Application.OnTime で設定した無限実行を正しく制御する

昼ごはんにカレーうどんを食べました。
メチャおいしかったけど、胸やけがコワかったあてくしです

こんちくわ 壁|ω・)ノ


さて、今日の記事はApplication.OnTimeメソッドで作った無限実行は止める仕組みが必要。でも 止めるなら正しく止めましょう~なオハナシです。

でわいきます ̄▽ ̄)ノ



目次


背景

前回の記事では、指定した時間にマクロを起動させるApplication.OnTimeメソッドを使って、学習時間を管理するマクロを紹介しました。

fukucyndip.hatenablog.com

アルゴリズムのポイントはApplication.OnTimeで Nowから指定の時間後に自分自身を起動する というものでした。ただし永久機関になってしまうので、ちゃんと止める仕組みを用意しましょうね~と書きました。

ところが、この止める仕組みとして前回紹介したプロシージャでは誤作動が起こることがわかりました。なぜだろうかと頭をひねり続けて土曜日の時間を延々と過ごしたのですが、一つの結論と対策にたどり着いたので、修正コードとともに紹介します。



Application.OnTime を使った時間更新アルゴリズム

例えば、10分間隔で表示時間を更新するなら、以下の様なコードが書けます。

f:id:FukuCyndiP:20200126215858p:plain:w550

 


このコードは以下のように動作して、A1セルの時間表示を更新していきます、

f:id:FukuCyndiP:20200126220007p:plain:w400



ただしコレはきちんと制御しないと、いつまでも時間更新が止まらないことになります。ところが、下の図のように、間に停止処理をただ挟み込むだけでは止めることができません。


f:id:FukuCyndiP:20200126220059p:plain:w450


Application.OnTimeメソッドが実行された瞬間に、次の実行予約が確定するためですね。 




永久機関を制御するために

そこで、Application.OnTimeメソッドを実行するフラグを使った以下のような仕組みを考えました(前回の記事で紹介した仕組み)。

f:id:FukuCyndiP:20200126220316p:plain:w450


このコードでは以下のような動作を期待しました。

f:id:FukuCyndiP:20200126220339p:plain:w450


結果、これでうまく動作したので、前回はこのまま記事を公開したのでした。




不具合発見

作成したコードで本格運用を始めたのですが、ふとカウンターが変な動作をするに気が付きました。

まずは、下記Gifアニメのカウンターの動きをご覧ください。


f:id:FukuCyndiP:20200126220410g:plain:w450

なんか、なんかカウンターが不規則な動きをしてます。

しかも、どう見ても設定の時間間隔ではない。。

原因を特定するのに時間がかかりましたが(土曜日の日中の時間をほぼ丸ごと使いましたw)、結論としてはカウンターのON/OFFを短時間の間に繰り返したことが原因で起こる不具合だということがわかりました。


では、何が起こっていたのか。

図にすると以下のような感じです。


f:id:FukuCyndiP:20200126220630p:plain:w500


先のコードでは、停止ボタンポチー後も発生するClockCounterの不要な予約起動そのものはスルーして、次の予約を発生させないという仕組みでしたが、結果的にそれが不具合の原因になっていたんですね。



不具合の対策

実いうと停止ボタンポチー後に発生するClockCounterの不要な予約起動は避けられないと思っていました。でも、ちゃーんと制御するための仕組みが準備されていました。

先日の記事を思い出してみましょう。

f:id:FukuCyndiP:20200126220806p:plain:w550
(Applicationは省略できません)

引数 省略 説明
EarliestTime プロシージャを実行したい時刻 Date
Procedure 実行するプロシージャ名(ダブルクオーテーションで囲みます)
(文字列を引数として渡す形での設定は可能だが、変数を渡す形での設定はできないよもよう)
String
LatestTime (指定のプロシージャが実行できない状態のとき)実行できる状態になるまで待つ時刻
省略した場合は指定のプロシージャが実行できるまで待ちます
Date
Schedule 設定済みのOnTimeメソッドの実行を取りやめるか否か Boolean


ということが書いてありましたね。

このうちの引数 [Schedule] のところを見ると、設定済みのOnTimeメソッドの実行を取りやめるか否かと書いてあります(ハイ。私が書きました)。これをきちんと使わなければいけなかったんですね。

でわどのように記述するのか。


f:id:FukuCyndiP:20200126220831p:plain:w500


御覧のように、引数 Schedule=False を記述します。ただし、ここで気を付けないといけないのは、実行をスキャンセルする予約起動を正確に特定しないといけないことです。つまり、起動するプロシージャ名だけでなく、起動時間の記述もそろえないといけないのですね。

というわけで, ClockCounterのコードの群は以下のように作りました。


f:id:FukuCyndiP:20200126221010p:plain:w500


Now()+時間間隔内容を変数reservedTimeに格納しました。そして、その中身を停止用プロシージャでも利用できるよう`モジュールレベル変数として宣言しています。

これで、停止ボタンポチー後に発生していた不要な予約起動も完全にキャンセルすることができるようになりました。

と、いうわけで前回掲載しましたコードは以下のように修正されました

Option Explicit

Private Const timeCount = "0:00:10"  '時間カウントの間隔を設定する
Private reservedTime As Date
Private isInitial As Boolean


Public Sub TimerStart()
  isInitial = True
  MsgBox "始めます", vbInformation, "がんばれ~"
  Call ClockCounter
End Sub


Private Sub ClockCounter()

  With ws時間記録
    '開始ボタンポチーした最初は時間の表示を更新しない
    If Not isInitial = True Then
      .Range("B2").Value = .Range("B2").Value + TimeValue(timeCount)
      .Range("C2").Value = .Range("C2").Value + TimeValue(timeCount)
    End If
    
    'ClockCounterご本尊
    reservedTime = Now() + TimeValue(timeCount)
    Application.OnTime reservedTime, "ClockCounter"
    isInitial = False
  End With

End Sub


Public Sub stopTimer()
  'タイマーを止めるときは "今回の学習時間" 列のみ時間をリセットする
  Application.OnTime reservedTime, "ClockCounter", , False
  
  MsgBox "記録終了します", vbInformation, "終わったのかな? ん?"
  ws時間記録.Range("B2").Value = "0:00:00"
End Sub



まとめ

今回の記事ではApplication.OnTimeメソッドの 自分予定呼び出しで作った無限実行を止める方法について書きました。前回の記事でもコードとしては掲載していたのですが、不具合が発生する不完全なものだということがわかりました。

Application.OnTimeメソッドには設定された予約実行を停止するための仕組みとして、引数 Schedule がちゃんと準備されており、それを使うことで不具合なくコードを実行することができるようになりました。


つまるところ、きちんと理解してから使えってことですねw

でわまた~  ̄▽ ̄)ノシ