【極限の最適化】MS Project VBAにおけるカスタムフィールド一括消去の高速化:COMオーバーヘッドと計算エンジンの制御
大規模なプロジェクト管理において、数千、時には数万のタスクやリソースを扱うことは珍しくない。エンジニアが直面する共通の課題の一つに、「カスタムフィールドの初期化(クリア)」がある。
多くの開発者は、`Application.ScreenUpdating = False` を記述し、`For Each` ループで各オブジェクトのプロパティを書き換えるという、教科書通りの実装で満足する。しかし、数千行のデータに対してこれを行うと、画面の更新は止まっていても、内部的な計算エンジンの再トリガーとCOM(Component Object Model)のオーバーヘッドにより、実行時間は指数関数的に増大していく。
本稿では、単なる「画面更新の抑制」を超え、Windows APIを用いた描画制御、Projectの計算エンジンの完全停止、そしてCOMオブジェクトのライフサイクルを考慮した、極限の高速化テクニックを解説する。
1. なぜ「標準的な手法」では不十分なのか
なぜ `ScreenUpdating = False` だけでは、プロフェッショナルな要求に応えられないのか。その理由は、MS Projectのアーキテクチャが持つ「3つの重み」にある。
1.1 計算エンジンの再トリガー(Calculation Engine)
MS Projectは、タスクのプロパティ(特に日付や期間、カスタムフィールド)が変更されるたびに、プロジェクト全体のスケジュール整合性を保つために計算エンジンを動かそうとする。`Calculation = pjCalculationAutomatic` の状態でループを回すと、1行書き換えるごとにプロジェクト全体が再計算されるという、最悪の計算量 $O(n^2)$ に陥る。
1.2 COM Interop のオーバーヘッド
VBAからProjectのオブジェクト(TaskやResource)にアクセスする際、VBAのランタイムとProjectのCOMコンポーネントの間で「境界」を越える通信が発生する。ループ内で `Task.Text1 = “”` と記述するたびに、この境界を跨ぐコストが発生する。
1.3 OSレベルの描画リクエスト
`ScreenUpdating = False` はあくまでアプリケーション層の制御だ。OSレベルでは、ウィンドウのハンドルに対して描画リクエストが送られ続けている場合があり、これがリソースを微量ながら消費し続ける。
2. アーキテクチャに基づいた高速化戦略
これらを打破するために、我々は以下の3段階の防御策を講じる。
1. Logical Layer: `Calculation` プロパティを `pjCalculationManual` に変更し、計算エンジンの暴走を止める。
2. Application Layer: `ScreenUpdating` を制御する。
3. OS Layer: Windows API (`SendMessage`) を用い、ウィンドウの描画更新(Redraw)を完全にロックする。
3. 実装:極限のクリア・プロシージャ
以下に、シニアエンジニアが現場に投入すべき、堅牢かつ高速なコードを示す。64bit環境(Office 2010以降の標準)を前提とした `PtrSafe` 宣言を用いている。
Option Explicit
' --- Windows API Declarations ---
' ウィンドウの描画更新を制御するための定数
Private Const WM_SETREDRAW As Long = &HB8
' SendMessage API: 特定のウィンドウハンドルに対してメッセージを送信する
' 64bit環境に対応するため PtrSafe を使用
If VBA7 Then
Private Declare PtrSafe Function SendMessage Lib "user32" Alias "SendMessageA" ( _
ByVal hwnd As LongPtr, _
ByVal wMsg As Long, _
ByVal wParam As LongPtr, _
ByVal lParam As LongPtr) As LongPtr
Private Declare PtrSafe Function FindWindow Lib "user32" Alias "FindWindowA" ( _
ByVal lpClassName As String, _
ByVal lpWindowName As String) As LongPtr
Else
Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" ( _
ByVal hwnd As Long, _
ByVal wMsg As Long, _
ByVal wParam As Long, _
ByVal lParam As Long) As Long
Private Declare Function FindWindow Lib "user32" Alias "FindWindowA" ( _
ByVal lpClassName As String, _
ByVal lpWindowName As String) As Long
End If
''' <summary>
''' 数千行のタスクに対してカスタムフィールドを高速にクリアする
''' </summary>
Public Sub FastClearTaskCustomFields()
Dim t As Task
Dim proj As Project
Dim hwnd As LongPtr
Dim originalCalculation As Long
Dim originalScreenUpdating As Boolean
Set proj = ActiveProject
' 1. 現在の状態をバックアップ(後で復元するため)
originalCalculation = proj.Calculation
originalScreenUpdating = Application.ScreenUpdating
' 2. ウィンドウハンドルの取得(MS Projectのメインウィンドウ)
' 注意: 環境によりクラス名は異なる可能性があるが、通常は "MSProject"
hwnd = FindWindow("MSProject", vbNullString)
On Error GoTo ErrorHandler
' --- 高速化開始フェーズ ---
' A. 計算エンジンの停止 (最重要)
proj.Calculation = pjCalculationManual
' B. アプリケーションレベルの更新停止
Application.ScreenUpdating = False
' C. OSレベルの描画ロック (Windows API)
If hwnd <> 0 Then
SendMessage hwnd, WM_SETREDRAW, 0, 0
End If
' --- メイン・ロジック ---
' COMオブジェクトへのアクセス回数を最小限にするため、
' ループ内では最小限のプロパティ操作に留める
For Each t In proj.Tasks
' 空の行(タスクが存在しない行)はスキップ
If Not t Is Nothing Then
' カスタムフィールド(Text1~Text30)を一括クリア
' ここでは例として Text1 と Text2 を対象とする
t.Text1 = ""
t.Text2 = ""
' 必要に応じて他のフィールドも追加
End If
Next t
' --- 高速化終了・復元フェーズ ---
CleanUp:
' 1. OSレベルの描画ロック解除
If hwnd <> 0 Then
SendMessage hwnd, WM_SETREDRAW, 1, 0
End If
' 2. アプリケーションレベルの更新再開
Application.ScreenUpdating = originalScreenUpdating
' 3. 計算エンジンの再開(自動計算に戻す)
proj.Calculation = originalCalculation
' プロジェクトの再計算を一度だけ実行して整合性を確保
proj.Calculate
MsgBox "高速クリアが完了しました。", vbInformation
Exit Sub
ErrorHandler:
MsgBox "エラーが発生しました: " & Err.Description, vbCritical
Resume CleanUp
End Sub
4. 技術的深掘り:プロフェッショナルのための知見
4.1 `SendMessage` による `WM_SETREDRAW` の威力
`Application.ScreenUpdating = False` は、Projectが「描画が必要だ」と判断した際に描画プロセスをスキップするよう指示するものだ。しかし、Windowsのメッセージキューには依然として描画要求が溜まり続けることがある。
`SendMessage hwnd, WM_SETREDRAW, 0, 0` を実行することで、OSレベルでそのウィンドウの描画更新を完全に無視させることができる。これにより、CPUリソースの無駄な消費を抑え、ループ処理の純粋な計算速度を極限まで引き出すことが可能になる。
4.2 オブジェクトのライフサイクルと `Nothing` チェック
大規模プロジェクトでは、タスクの間に「空白行」が存在することがある。`For Each t In proj.Tasks` で回す際、`t` が `Nothing` であるケースを無視せずに `If Not t Is Nothing Then` でガードすることは、ランタイムエラーを防ぐだけでなく、予期せぬメモリエラーを回避するための「作法」である。
4.3 メモリ管理の思想
VBAにおけるCOMオブジェクトの解放は、通常、スコープを抜ける際に行われるが、極めて巨大なループ内で大量のオブジェクトを生成・操作する場合、メモリ圧迫(Memory Pressure)が懸念される。本コードでは `Task` オブジェクトを再利用する `For Each` を採用しているが、もしカスタムオブジェクトを自作して生成する場合は、必ず `Set obj = Nothing` を明示的に行い、参照カウントを適切に管理せよ。
結論
高速な自動化ツールとは、単にコードが短いものではない。対象となるソフトウェア(MS Project)の内部動作、およびそれを動かしているOS(Windows)の振る舞いを理解し、それらを制御下に置いたコードである。
`Calculation = pjCalculationManual` による論理的停止、`ScreenUpdating` によるアプリケーション制御、そして `SendMessage` によるOSレベルの制御。この三段構えこそが、レガシーな環境であっても、現代の巨大なデータセットであっても、圧倒的なパフォーマンスを実現するための「エンジニアリングの正解」である。
