こんにちは!VBAの世界へようこそ。
マクロの記録から一歩踏み出し、「もっと業務をスマートに自動化したい!」と熱意を燃やしているあなたへ。
今回は、Microsoft ProjectのVBA(Project VBA)において、多くの開発者が知らず知らずのうちにハマってしまう「パフォーマンスの罠」と、それを華麗に回避する「Taskオブジェクトのキャッシュ戦略」についてお話しします。
ここをクリアすれば、あなたの書くコードは一気にプロの領域へジャンプアップします。
優しく、かつ本質的なところまで深く解説していきますので、ぜひ最後までついてきてくださいね!
—
1. なぜProject VBAのループは遅くなるのか?
まずは、私たちが日常的に書く「タスクをぐるぐる回すループ処理」を思い浮かべてみてください。WBSの全タスクに対して何らかの処理を行うとき、次のようなコードを書いたことはありませんか?
‘ 【アンチパターン】毎回タスクコレクションにアクセスする例
Dim i As Long
For i = 1 Wb ActiveProject.Tasks.Count
‘ 毎回 `.Tasks(i)` と呼び出している
If ActiveProject.Tasks(i).Name <> “” Then
ActiveProject.Tasks(i).Text1 = “処理済み”
End If
Next i
一見、何の問題もなさそうなこのコード。しかし、タスク数が数千件規模になってくると、「あれ、終わらない……固まった?」という現象が発生します。
原因はCOMオブジェクトの「通訳コスト」にある
VBAからMicrosoft Projectの本体(COMオブジェクト)を操作するとき、VBAとProjectの間には目に見えない「通訳」が立っています。
- あなたのコード:「ねえ、$i$番目のタスクをちょうだい!」
- VBA&Project:「はいよ、わざわざメモリを探して持ってきたよ!」
これをループのたびに(例えば5,000回も)行っているとしたらどうでしょう?
タスクの数が増えれば増えるほど、この「お使いコスト(オーバーヘッド)」が雪だるま式に膨れ上がり、処理が極端に重くなってしまうのです。これが、Project VBAにおける最大のパフォーマンス殺人事件の正体です。
—
2. 救世主「キャッシュ戦略」とは何か?
この問題を一発で解決するのが、今回伝授する「Taskオブジェクトのキャッシュ戦略」です。
考え方はとてもシンプル。
「最初に一度だけ、すべてのタスクをVBA側のメモリ(配列やコレクション)にごっそり持ち帰る(キャッシュする)。あとは、その手元にあるメモリ上のデータだけを高速に料理する」 というアプローチです。
イメージとしては、毎回わざわざ遠くの巨大な倉庫(Projectのデータベース)まで荷物を取りに行くのではなく、手元のデスク(VBAのメモリ)に必要な分をダンボール箱でドカンと用意しておくようなものです。
—
3. 実践!処理速度を10倍にするコード例
それでは、実際に現場でそのまま使える、極上の最適化コードを見てみましょう。
今回は、プロジェクト内の全タスクを走査し、まだ完了していないタスクの「Text1」フィールドに特定のフラグを立てる処理を例にします。
Sub OptimizeTaskProcessing()
Dim t As Task
Dim startTime As Double
startTime = Timer ‘ 処理時間計測用
‘ 【重要】画面描画を停止して爆速化するおまじない
App.ScreenUpdating = False
On Error GoTo ErrorHandler
‘ 1. タスクが存在するかチェック
If ActiveProject.Tasks.Count = 0 Then
MsgBox “タスクが存在しません。”, vbExclamation
GoTo Finally
End If
‘ 2. 【キャッシュ戦略の核心】
‘ あらかじめDictionaryや配列、あるいはFor Eachによる参照保持を利用する
‘ 今回は最も手軽かつ強力な「For Each」によるオブジェクト参照のローカル保持を使う
Dim targetTask As Task
Dim processedCount As Long
processedCount = 0
‘ For Eachを使うことで、Projectは内部的に効率よくオブジェクトの参照をVBAに渡してくれます
For Each targetTask In ActiveProject.Tasks
‘ Nothing判定(空行などの対策)
If Not targetTask Is Nothing Then
‘ サンプル条件:サマリータスクではなく、かつ未完了のもの
If Not targetTask.Summary Then
If targetTask.PercentComplete < 100 Then
' キャッシュされたオブジェクトを直接操作(ドットつなぎの回数を最小限に)
targetTask.Text1 = "要確認"
processedCount = processedCount + 1
End If
End If
End If
Next targetTask
' 処理時間の表示(どれだけ速くなったか実感しよう!)
MsgBox "処理が完了しました!" & vbCrLf & _
"処理件数: " & processedCount & "件" & vbCrLf & _
"実行時間: " & Format(Timer - startTime, "0.00秒"), vbInformation
Finally:
' 画面描画を元に戻す
App.ScreenUpdating = True
Exit Sub
ErrorHandler:
MsgBox "予期せぬエラーが発生しました: " & Err.Description, vbCritical
Resume Finally
End Sub
コードのここがポイント!
1. `For Each` の活用:
番号(インデックス)で `Tasks(i)` を指定するのではなく、`For Each targetTask In ActiveProject.Tasks` を使うことで、Project内部のイテレータが最適に働き、無駄なオブジェクト生成を回避できます。
2. `App.ScreenUpdating = False`:
VBA高速化の三種の神器の一つ。画面の書き換えを一時停止することで、描画処理の待ち時間を削り取ります。
3. ドットつなぎの最小化:
`ActiveProject.Tasks(i).PercentComplete` のように何度もルートから呼び出すのではなく、変数 `targetTask` に一度受けることで、メモリへのアクセスを最小限に抑えています。
—
4. 陥りやすい罠と注意点
完璧に見えるキャッシュ戦略ですが、実務で使う際にはいくつか気をつけておべきポイントがあります。
⚠️ 注意点1:処理途中の「タスクの削除・追加」にご用心
ループの最中に、キャッシュしているタスクを削除したり、新しいタスクを追加したりすると、メモリ上の参照と実際のProject上のインデックスがズレてしまい、ランタイムエラー(エラー 91: オブジェクト変数または With ブロック変数が見つかりません)を引き起こします。
- 対策: タスクの構造を変更するような処理(追加・削除)は、ループ内では行わず、削除対象のIDを一度別の配列などに溜めておき、ループを抜けたあとにまとめて削除するようにしましょう。
⚠️ 注意点2:メモリの解放を意識する
巨大なプロジェクト(数万行規模のWBS)を扱う場合、オブジェクト変数を大量に保持し続けるとメモリを圧迫します。処理が終わったら、ループ変数やオブジェクト変数は必要に応じて `Set targetTask = Nothing` などでクリーンアップする意識を持つと、より堅牢なコードになります。
—
まとめ:ここをクリアすれば、Project VBAの基本はバッチリ!
今回は、Project VBAにおける「コレクション操作の高速化:Taskオブジェクトのキャッシュ戦略」について解説しました。
- なぜ遅いのか? → ループのたびにCOMオブジェクト(通訳)を呼び出しているから。
- どう解決するのか? → 変数や `For Each` を使って手元にオブジェクトをキャッシュし、アクセス回数を極限まで減らす。
- おまけのテクニック → `App.ScreenUpdating = False` で描画を止めてさらに加速させる。
この原則を知っているだけで、あなたが書くマクロの実行速度は劇的に変わり、「あれ、もう終わったの?」という快感を味わえるはずです。
ここをクリアできれば、もうあなたは「マクロの記録をちょこっと直すだけの初心者」ではありません。胸を張って「業務自動化エンジニア」と名乗ってください。
あなたのVBAライフが、より快適で知的でエキサイティングなものになりますように。
それでは、次の現場でお会いしましょう!
