【実務・中級編】【上級者向け】Project VBAにおける非同期処理のシミュレーションとUIフリーズの回避 – Project VBA解析バイブル

スポンサーリンク

【Project VBA極限知見】大量プロジェクト処理の呪縛を断つ:非同期シミュレーションとUIフリーズ完全回避の技術

プロジェクトマネジャーやPMOの皆さん、日々の業務でお疲れ様です。
何十、何百というサブプロジェクトファイルをバッチ処理で一斉に更新・ベースライン設定するマクロを組んだはいいものの、「処理が始まった瞬間にExcelやProjectの画面が白濁し、『応答なし』の文字に肝を冷やした」「進捗が全く見えず、本当に動いているのか不安で強制終了してしまった」――そんな悪夢を経験したことはないでしょうか。

MS ProjectのVBAエンジンはシングルスレッドで動作します。そのため、巨大なスケジュール群に対して重たいループを回すと、UIスレッドが完全に占有され、OSから「フリーズしたアプリ」と判定されてしまいます。

今回は、Project VBAの限界を突破し、滑らかな進捗バーと「キャンセル可能」な堅牢なバッチ処理を実現するための非同期シミュレーションとUIフリーズ回避の極意を伝授します。

1. なぜ愚直なループは地獄を見るのか?

多くの開発者がやりがちな失敗は、以下のような単純な`For Each`ループです。

‘ 【アンチパターン】これでは画面がフリーズし、ユーザーは絶望する
Dim prj As Project
For Each prj In Projects
‘ 重たいタスク操作やベースライン設定
Call HeavyProcess(prj)
Next prj

Project VBAにおいて、プロジェクトを開き、タスクを操作し、ベースラインを設定するプロセスは、COMオブジェクトを介した重厚な処理の連続です。これを画面描画(UIスレッド)の更新を一切挟まずに実行すると、OSはメッセージキューを処理できなくなり、UIが凍結します。

ここに介入するのが、「意図的な制御権の解放(Yield)」「非同期シミュレーション」の概念です。

2. 堅牢なUI制御の要:`DoEvents` とタイマー制御

VBAには本格的なマルチスレッド機能(`Task`や`Thread`クラス)はありません。しかし、`DoEvents`関数を適切に配置することで、OSに制御権を一時的に返し、UIの再描画やユーザーからの入力を受け付けることが可能です。

ただし、`DoEvents`は万能薬ではありません。「毎回のループで呼び出すと、逆にオーバーヘッドで処理速度が極端に低下する」という致命的なトレードオフがあります。したがって、「N回に1回、または一定時間経過時のみに絞って呼び出す」のがプロのアーキテクチャ設計です。

3. 【プロダクションコード】実務で使える堅牢なバッチ処理エンジン

以下のコードは、複数のプロジェクトファイルをサイレントオープンし、ベースラインを設定した上で、ユーザーフォームによるプログレス表示と「キャンセル」機能を持たせた実用コードです。

ユーザーフォームの準備(事前準備)

VBAプロジェクトに `frmProgress` という名前のUserFormを作成し、以下のコントロールを配置してください。

  • `lblStatus` (Label): 現在の処理状況を表示
  • `barProgress` (ProgressBar もしくは Width可変のLabel): 進捗バー
  • `btnCancel` (CommandButton): キャンセルボタン

標準モジュールの実装コード

Option Explicit

‘ キャンセルフラグ(モジュールレベル変数)
Public g_IsCancelled As Boolean

”’

”’ 複数プロジェクト一括ベースライン設定・非同期シミュレーション実行メイン
”’

Public Sub ExecuteBatchBaselineProcessing()
Dim targetFolder As String
targetFolder = “C:\ProjectData\BatchTarget\” ‘ 対象フォルダのパス

Dim fso As Object
Set fso = CreateObject(“Scripting.FileSystemObject”)

If Not fso.FolderExists(targetFolder) Then
MsgBox “対象フォルダが存在しません: ” & targetFolder, vbCritical
Exit Sub
End If

Dim targetFiles As Object
Set targetFiles = fso.GetFolder(targetFolder).Files

Dim totalFiles As Long
totalFiles = targetFiles.Count

If totalFiles = 0 Then
MsgBox “処理対象のファイルがありません。”, vbInformation
Exit Sub
End If

‘ キャンセルフラグの初期化
g_IsCancelled = False

‘ 進捗フォームの起動(モードレス表示が必須)
Load frmProgress
frmProgress.Show vbModeless
frmProgress.InitializeProgress totalFiles

‘ Projectの警告や画面描画を抑制して爆速化
Application.DisplayAlerts = False
Application.ScreenUpdating = False

Dim file As Object
Dim currentCount As Long
currentCount = 0

On Error GoTo ErrorHandler

For Each file in targetFiles
‘ 拡張子がProjectファイル(.mpp)の場合のみ処理
If LCase(fso.GetExtensionName(file.Name)) = “mpp” Then
currentCount = currentCount + 1

‘ 1. UIの生息確認とキャンセルチェック
DoEvents
If g_IsCancelled Then
MsgBox “ユーザーによって処理がキャンセルされました。”, vbExclamation, “中断”
Exit For
End If

‘ 2. ステータス更新
frmProgress.UpdateProgress currentCount, “処理中: ” & file.Name

‘ 3. プロジェクトのオープン(バックグラウンド/非表示推奨だがProjectには隠しオープンがないためReadOnly等で配慮)
Dim prj As Project
Set prj = Projects.Open(file.Path, ReadOnly:=False)

‘ 4. 重たい実処理(ベースライン設定の例)
Call ApplyBaselineToProject(prj)

‘ 5. 保存して閉じる
prj.Save
prj.Close pjDoNotSave
End If
Next file

CleanUp:
‘ 描画とアラートの復元
Application.DisplayAlerts = True
Application.ScreenUpdating = True

‘ フォームの破棄
Unload frmProgress

If Not g_IsCancelled Then
MsgBox “すべてのバッチ処理が正常に完了しました。”, vbInformation, “完了”
End If
Exit Sub

ErrorHandler:
MsgBox “予期せぬエラーが発生しました。” & vbCrLf & _
“エラー番号: ” & Err.Number & vbCrLf & _
“説明: ” & Err.Description, vbCritical, “システムエラー”
Resume CleanUp
End Sub

”’

”’ プロジェクト個別のベースライン設定ロジック
”’

Private Sub ApplyBaselineToProject(ByRef prj As Project)
‘ 例としてベースライン1を設定
prj.SetBaseline Baseline:=pjBaseline1

‘ 独自のカスタムフィールド計算や複雑なロジックをここに記述
‘ Call AnotherComplexFunction(prj)
End Sub

ユーザーフォーム(`frmProgress`)側のコード

Option Explicit

”’

”’ プログレスバーの初期化
”’

Public Sub InitializeProgress(ByVal maxCount As Long)
Me.barProgress.Tag = maxCount
Me.barProgress.Width = 0
Me.lblStatus.Caption = “処理を準備しています…”
Me.Repaint
End Sub

”’

”’ 進捗状況の動的更新
”’

Public Sub UpdateProgress(ByVal currentCount As Long, ByVal message As String)
Dim maxCount As Long
maxCount = CLng(Me.barProgress.Tag)

If maxCount > 0 Then
‘ 簡易的にラベルの幅で進捗バーを表現(最大幅を300と仮定)
Dim maxWidth As Double
maxWidth = 300
Me.barProgress.Width = (currentCount / maxCount) maxWidth
End If

Me.lblStatus.Caption = message
Me.Repaint ‘ 強制描画
End Sub

”’

”’ キャンセルボタン押下時のイベント
”’

Private Sub btnCancel_Click()
If MsgBox(“処理を中断しますか?”, vbYesNo + vbQuestion, “確認”) = vbYes Then
g_IsCancelled = True
Me.lblStatus.Caption = “キャンセル処理中…”
End If
End Sub

4. チーフアーキテクトが教える、現場で絶対にハマる「3つの罠」

実務でこのコードを適用する際、以下のポイントを押さえておかないと痛い目に遭います。

1. `vbModeless` の絶対厳守
フォームを `Show` する際、`vbModal`(デフォルト)にすると、コードの実行がそこでストップし、後ろのループが回りません。必ず `vbModeless` で非同期的にフォームを浮かせ、背後でVBAコードを走らせてください。
2. `DoEvents` の過剰摂取に注意
今回のコードではファイルループの1回ごとに `DoEvents` を挟んでいますが、もし1ファイルあたりの処理がミリ秒単位で終わるような軽量なものなら、`If currentCount Mod 10 = 0 Then DoEvents` のように間引く設計にしてください。
3. エラーハンドリング時のCOMオブジェクト解放
バッチ処理中にエラーが発生して `On Error` でジャンプした際、開いていたプロジェクトが中途半端なロック状態になることがあります。必ず `Application.DisplayAlerts` などを復元し、メモリリークやファイルロックを防ぐクリーンアップ動線を確保してください。

5. まとめ

Project VBAにおける大量データ処理の最適化は、単に「動くコードを書く」次元から、「UX(ユーザー体験)とシステム安定性を両立させる」アーキテクチャの次元へとステップアップする必要があります。

今回紹介した `vbModeless` フォームによる進捗可視化`DoEvents` による制御権の巧妙なコントロール をマスターすれば、ユーザーからの「フリーズしたんだけど!」というクレームに怯える必要はもうありません。

あなたの現場の業務自動化を、ワンランク上の「プロフェッショナル・グレード」へと引き上げてください。

タイトルとURLをコピーしました