【入門編】Project VBAにおける「コレクション」操作の高速化:Taskオブジェクトのキャッシュ戦略 – Project VBA解析バイブル

スポンサーリンク

こんにちは!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ライフが、より快適で知的でエキサイティングなものになりますように。
それでは、次の現場でお会いしましょう!

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