【上級者向け】MS Project VBA高速化の極意:Collectionオブジェクトによるメモリ内一括処理の全貌
開発現場でMS ProjectのVBAを書くとき、こんな絶望感を味わったことはないだろうか。
「数千行のタスクに対して、ループを回してカスタムフィールドを書き換えていたら、コーヒーを飲み干しても終わらない」
MS Projectのオブジェクトモデルは、残念ながら洗練されているとは言い難い。VBAから `ActiveProject.Tasks` を直接叩き、セル(Field)の読み書きを行うコードは、文字通り「ディスクI/OやGUI描画を伴う遅延」の爆弾を抱えている。`.Cells` プロパティやタスクプロパティにアクセスするたびに、COMの境界を跨ぐオーバーヘッドが発生し、処理速度は劇的に低下するのだ。
真にスケーラブルで、エンタープライズの現場に耐えうるツールを作りたいのであれば、アプローチを変えなければならない。
「Projectのオブジェクトを直接触るな。データを一度メモリ上の `Collection`(またはDictionary)に吸い上げ、極限まで高速化した上で、最後に一撃で書き戻せ。」
今回は、このメモリ内高速処理アーキテクチャの全貌を、プロダクション品質のコードとともに伝授しよう。
—
1. なぜ「直接操作」は遅いのか?(オブジェクトモデルの闇)
MS ProjectのVbAにおいて、以下のコードは「やってはいけないアンチパターン」の典型だ。
‘ 【悪夢のアンチパターン】絶対にしてはいけない直接操作
Dim t As Task
For Each t In ActiveProject.Tasks
If Not t Is Nothing Then
If t.Text1 = “要確認” Then
t.Text1 = “対応中”
t.Cost = t.Cost 1.1
End If
End If
Next t
何がいけないのか。
1. COM境界の往復: `t.Text1` や `t.Cost` にアクセスするたびに、VBAのランタイムからProjectのネイティブエンジンへリクエストが飛ぶ。
2. GUI/イベントの再描画: プロパティを変更するたびに、ガントチャートやリソースプールへの影響評価、画面の再描画(あるいはその抑制判定)が走る。
これを数万件のタスクで行えば、VBAがフリーズしたように見えるのは当然だ。
解決策:メモリ内キャッシュ戦略
対策はシンプルだ。
1. 読込フェーズ: 一度だけ `Tasks` コレクションを走査し、必要な情報(ID、UID、各種プロパティ)を、VBA側で用意した軽量な自作クラス(UDTでも可)に格納し、それを `Collection` に蓄える。
2. 演算フェーズ: MS Projectのオブジェクトには一切触れず、メモリ上の `Collection` 内だけで高速にデータを加工・計算する。
3. 書込フェーズ: 計算結果を基に、MS Projectへ一括(または変更があったものだけピンポイントで)書き戻す。
このアプローチにより、COMとの通信回数を最小限に抑え、劇的な速度向上(体感で10倍〜50倍以上)を実現できる。
—
2. 堅牢な設計:データ構造の定義
メモリ内でタスク情報を安全かつスマートに取り扱うため、まずは「軽量データコンテナ」としてのクラスモジュールを定義する。
(※UDT(ユーザー定義型)でも良いが、Collectionのキー管理や拡張性を考慮し、ここでは軽量クラスモジュールを採用する)
クラスモジュール名: `C_TaskData`
Option Explicit
‘ 保持すべきタスクのプロパティ(必要に応じて拡張)
Public TaskID As Long
Public TaskUID As Long
Public TaskName As String
Public Text1 As String
Public Cost As Double
Public IsDirty As Boolean ‘ 変更フラグ(無駄な書き込みを防ぐため)
—
3. プロダクションコード:メモリ内一括処理の実装
ここからが本題だ。外部ファイルやデータベース、あるいは社内ニッチなロジックに基づき、数千件のタスクを一括で処理するプロシージャの完全版を提示する。
保守性を高めるため、「画面描画の抑制(`ScreenUpdating`)」「計算方法の手動化(`Calculation`)」を組み合わせることで、さらに処理を最適化している。
Option Explicit
Public Sub ExecuteHighSpeedTaskProcessing()
Dim startTime As Double
startTime = Timer
‘ 1. 環境の最適化(描画と自動計算をストップ)
Dim oldScreenUpdate As Boolean
Dim oldCalcMode As Long
oldScreenUpdate = Application.ScreenUpdating
oldCalcMode = Application.Calculation
Application.ScreenUpdating = False
Application.Calculation = pjCalculationManual ‘ 自動計算をオフに
On Error GoTo ErrorHandler
Dim prj As Project
Set prj = ActiveProject
‘ 2. 読込フェーズ:メモリ上のCollectionへ展開
Dim taskCollection As Collection
Set taskCollection = New Collection
Dim t As Task
Dim taskData As C_TaskData
Dim totalTasks As Long
totalTasks = prj.Tasks.Count
Debug.Print “— 読込開始: ” & totalTasks & ” タスク —”
For Each t In prj.Tasks
If Not t Is Nothing Then
‘ 削除済みタスクやサマリータスクの除外判定を入れることも可能
Set taskData = New C_TaskData
taskData.TaskID = t.ID
taskData.TaskUID = t.UID
taskData.TaskName = t.Name
taskData.Text1 = t.Text1
taskData.Cost = t.Cost
taskData.IsDirty = False
‘ UIDをキーとしてCollectionに格納(O(1)でのアクセスが可能に)
taskCollection.Add taskData, “K_” & CStr(t.UID)
End If
Next t
‘ 3. 演算フェーズ:メモリ内だけで爆速処理
Debug.Print “— 演算開始 —”
Dim item As Variant
For Each item in taskCollection
Set taskData = item
‘ 【ビジネスロジックの例】
‘ 例:Text1が特定の条件の場合、コストを10%アップし、Text1を書き換える
If taskData.Text1 = “要見直し” Then
taskData.Cost = taskData.Cost 1.1
taskData.Text1 = “自動調整済”
taskData.IsDirty = True ‘ 変更ありフラグを立てる
End If
‘ 例:特定のコスト超過タスクにフラグを立てる
If taskData.Cost > 1000000 And taskData.Text1 <> “自動調整済” Then
taskData.Text1 = “高額案件”
taskData.IsDirty = True
End If
Next item
‘ 4. 書込フェーズ:変更があったものだけProjectへ一括反映
Debug.Print “— 書込開始 —”
Dim writeCount As Long
writeCount = 0
For Each item in taskCollection
Set taskData = item
If taskData.IsDirty Then
‘ UIDから実際のProjectタスクを特定して書き戻す
‘ (FindTaskUIDを使用することで、IDがズレるリスクを完全に排除)
Set t = prj.Tasks.UniqueID(taskData.TaskUID)
If Not t Is Nothing Then
t.Text1 = taskData.Text1
t.Cost = taskData.Cost
writeCount = writeCount + 1
End If
End If
Next item
‘ 5. 終了処理
Application.ScreenUpdating = oldScreenUpdate
Application.Calculation = oldCalcMode
Debug.Print “— 処理完了 —”
Debug.Print “総処理時間: ” & Format(Timer – startTime, “0.00秒”)
Debug.Print “更新タスク数: ” & writeCount
MsgBox “高速処理が完了しました。” & vbCrLf & “処理時間: ” & Format(Timer – startTime, “0.00秒”) & “秒”, vbInformation
Exit Sub
ErrorHandler:
‘ 異常終了時も必ず環境を元に戻す
Application.ScreenUpdating = oldScreenUpdate
Application.Calculation = oldCalcMode
MsgBox “エラーが発生しました: ” & Err.Description, vbCritical
End Sub
—
4. プロフェッショナルが教える実装上の注意点
このアーキテクチャを現場のツールとして組み込む際、シニアエンジニアとして知っておくべき「罠」と「対策」を共有しよう。
① `Task.ID` ではなく `Task.UID` をキーに使うべき理由
MS Projectにおいて、`ID`(行番号的なインデックス)はタスクの挿入・削除によってダイナミックに変動する。ループの途中でIDをキャッシュしても、書き戻す瞬間にズレが生じるリスクがある。
そのため、絶対に不変の識別子である `UID`(UniqueID)をキーとしてCollectionに格納し、書き戻し時は `prj.Tasks.UniqueID(uid)` でアクセスすること。これがバグをゼロにする鉄則だ。
② 大規模データにおけるCollectionの限界
数万件程度であれば `Collection` や `Scripting.Dictionary` で何ら問題ない。しかし、10万件を超えるような超巨大プロジェクトファイルを扱う場合、VBAのCollectionはメモリ効率の面でボトルネックになることがある。
もしパフォーマンスが頭打ちになった場合は、VB.NET等でCOMアドイン(VSTO)として構築し、ネイティブなメモリ管理を行うべき境界線だと判断してほしい。
③ データベースや外部ファイル(Excel/CSV)との連携
このメモリ内Collection構造の最大の強みは、「MS Project以外のデータソースと容易にマージできること」だ。
例えば、外部のSQL ServerやExcelから取得した最新のコスト情報を、一度別のCollection(あるいはDictionary)に読み込み、Project由来のCollectionと `TaskUID` で突き合わせ(マージ)てから、一気にProjectへ書き戻す。
この「データソースの抽象化とメモリ上での結合」ができるようになれば、あなたも立派なVBAアーキテクトだ。
—
終わりに:ツール作成の「品格」
業務自動化ツールを作るということは、単に「動けばいいコード」を乱雑に貼り付けることではない。
ユーザーの貴重な時間を奪わないこと。メンテナンスする後任者に呪われないコードを書くこと。そして、システムのリソース(メモリやCPU)に対して敬意を払うこと。
今回紹介した「Collectionを用いたメモリ内一括処理」は、MS Project VBAにおける最もエレガントな解決策の一つだ。ぜひあなたのプロジェクトに導入し、その圧倒的なスピードの違いを体感してほしい。
