DoEvents関数
DoEvents関数はVBAの処理を一時中断させてExcelの操作を行うことができるようになります。
通常、VBAの処理中はExcelの操作は出来ません。しかし、DoEvents関数を使うとVBAの処理を離れ、Excelの操作が可能になります。
用途としては、ループ処理中に途中で処理を止めたい場合やプログレスバーの表示などが挙げられます。Ctrl + Breakキーを押してVBAの処理を止めようと思っても止まらない事象に遭遇したことがあると思いますが、それも止まってくれます。
DoEvents関数でWindows側に制御が渡されたあとは、OSで管理しているイベントキューのキーイベントの全てのキーが送信されるとVBAに制御が戻ります。WindowsAPIのプログラミングやOSのタスク管理などが分からないとこの意味は分かりにくいかもしれませんが、分からなくても気にしなくていいです。意味が分からない方は0.1秒未満ぐらいDoEventsで待ってからVBAの処理が続行する、と思ってください。
VBA処理中もExcelの操作を行いたい場合にはDoEvents関数は有効ですが、上記の通り、Windowsの処理待ちが発生するためVBAの処理が長くなるなどのいくつかの欠点があります。
構文
DoEvents() As Integer
引数はありません。
戻り値はその時点で開いているフォームの数を返します。フォームを開いていない場合は0が返ります。通常は戻り値を利用することがないため、戻り値を受け取らずに関数名だけをコードに書くことが多いです。
DoEvents関数の欠点
DoEvents関数にはいくつか欠点があります。
- VBAの処理が遅くなる。(対応方法:GetInputState関数(Win32API)を使う)
- 想定外のエラーが頻発する。(対応方法:On Error Resume Nextでエラー回避する)
- イベント処理は対象のブックに書く必要がある。(対応方法:イベント(ボタン押下やセル値変更など)が発生するような実装をしない)
これらの欠点から、重要性が高い処理の場合はDoEvents関数を使わないという判断をした方がいい場合がありますので、無理にDoEvents関数を使うよりも本来行うべき処理を優先させるかの判断が必要になることもあります。
ただ、それぞれの欠点には一応対応方法はあるため、ある程度は回避できます。
具体的には、DoEvents関数を使うときは、On Error Resume Nextは必須で、さらに高速化を求めるならGetInputState関数の利用することをお勧めします。
以下のサンプルコードは上記の欠点に対応しています。
サンプルコード
ループ内でDoEvents関数を呼び出してWindowsに処理を渡すサンプルです。
Windowsに処理が渡るようになっているため、ワークシートでの操作が可能です。B1セルに”a”を入力すると処理が止まります。
詳細は後述しますが、エラーでVBA処理が終了することやDoEvents関数の呼び出し回数が多いことによる処理が遅くなる点への対応も入れています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
'// 64bit版 #If VBA7 And Win64 Then Private Declare PtrSafe Function GetInputState Lib "user32" Alias "GetInputState" () As Long '// 32bit版 #Else Private Declare Function GetInputState Lib "user32" () As Long #End If Sub DoEventsTest() On Error Resume Next Dim i Dim dTimer '// 0時からの経過秒数 Dim dElapsed '// 前回Timer関数実行時からの経過秒数 '// セルの入力値を削除 Cells.Clear i = 0 m_bButtonClick = False dTimer = Timer '// 横(列)ループ Do '// イベント発生時、または、1秒経過した場合 If (GetInputState() Or dElapsed > 1) Then '// Windowsに制御を渡す DoEvents '// B1セルに"a"を入力 If (Range("B1").Value = "a") Then '// ループを抜ける Exit Do End If '// DoEvents用ループカウンタを初期化 i = 0 dTimer = Timer End If '// A1セルにループカウンタ値をセット Range("A1").Value = CStr(i) '// DoEvents用ループカウンタを加算 i = i + 1 '// 経過秒数 dElapsed = Abs(Timer - dTimer) '// エラー発生時 If Err.Number <> 0 Then '// イミディエイトウィンドウにエラーを出力 Debug.Print Err.Number & " " & Err.Description '// エラーをクリア Err.Clear End If Loop End Sub |
コード説明
処理の内容はコメントの通りなので省略します。
コードの先頭でWin32APIのGetInputState()を2つ書いていますが、Win32APIは32bit版と64bit版で定義が異なるためです。
Win32APIの32bit版を64bit版にしたい場合の詳細については「VBAでWin32API(WindowsAPI)を64bit対応する方法」をご参照ください。
Windowsに制御が移ったときのエラーの対策と、DoEvents関数が遅い問題の対策について補足説明します。
エラー対策
上で「DoEvnetsはエラーが頻発する」と書いていますが、具体的には、DoEvents関数でExcel上での操作は可能になりますが、実際にセルに”a”とかの文字列を入力すると、入力途中の状態で止まりエラー1004が発生して実行していたVBAの処理自体が終了します。
このように、DoEvents関数でWindowsに制御が移ったとしても、そこでどういう操作が可能なのかはかなり制限があります。少なくとも、セルへの入力はエラーが頻発します。
そこで、「On Error Resume Next」を使ってエラーが起きても処理を続行するようにしています。なお、エラーが発生した場合はイミディエイトウィンドウにエラー内容を出力しています。
なお、マクロ実行時に「コンパイルエラー 定数、固定長文字列、配列、ユーザー定義型および Declareステートメントは、オブジェクトモジュールのパブリックメンバーとしては使用できません。」とのエラーが出た場合は「Private Declare Function GetInputState Lib “user32” () As Long」の記述で先頭の「Private」が書いてあるかを確認してください。
処理遅延対策
待機中のイベントがあるかどうかの判定にWin32APIのGetInputState関数を利用して、イベントがあるときだけDoEvents関数を呼び出すようにしています。
ただ、マクロを実行後に数秒(体感では5秒程度)経過すると、それ以降では待機中のイベントの検出を行えないのかDoEvents関数が呼び出されず、Excelブックが応答しなくなってしまいます。こうなるとタスクマネージャ等でExcelを強制終了するかVBAの処理が終わるまで待つかになります。
そのため、別でTimer関数を使って経過秒数を取得し、1秒ごとにDoEvents関数を呼び出して応答なしの状態になるのを回避しています。
まとめ
上に書いたことの繰り返しになりますが、DoEvents関数を使うときはWin32APIのGetInputState関数と「On Error Resume Next」でのエラー回避はセットで実装することをお勧めします。