—
【Project VBAの深淵】`Application.ActiveProject` に依存するな。複数ファイル環境で「事故」を防ぐための鉄則
Microsoft Project(以下、MS Project)の自動化に取り組む際、多くの開発者が最初に手に取る武器が `ActiveProject` だ。しかし、現場の最前線で数多の炎上プロジェクトを鎮火してきた私から言わせれば、`ActiveProject` を多用するコードは、いつ爆発してもおかしくない時限爆弾を抱えているに等しい。
特に、複数の `.mpp` ファイルを同時に開き、リソースの共有やマスタープロジェクトを扱う実務環境において、この「アクティブなものに依存する」という甘い考えは、データ破壊という最悪の結末を招く。
今回は、Project VBAの基礎でありながら、プロフェッショナルへの登竜門とも言える「プロジェクトオブジェクトの厳密な特定と管理」について、その真髄を伝授しよう。
—
1. なぜ `ActiveProject` は「悪」なのか
初心者が書くコードの典型例はこうだ。
「今見ているこのプロジェクトの、このタスクを更新したい」から、`ActiveProject` を使う。
‘ アンチパターンの典型
Sub UpdateProject_Bad()
‘ 実行中にユーザーが別のウィンドウをクリックしたら?
ActiveProject.Tasks.Add Name:=”新規タスク”
End Sub
このコードの致命的な欠陥は、「実行時のコンテキスト(文脈)をユーザーの操作に委ねている」点にある。
MS Project VBAは、ExcelやWord以上に複雑なリソース計算エンジンを持っている。マクロの実行中にユーザーがふと別のプロジェクトのウィンドウに切り替えた瞬間、`ActiveProject` はその切り替わった先のプロジェクトを指してしまう。
その結果、プロジェクトAを更新するつもりが、全く関係のないプロジェクトBのスケジュールを書き換えてしまう。これが大規模な統合スケジュール管理であれば、その損害は計り知れない。
—
2. 堅牢な設計:プロジェクトを「変数」に閉じ込める
プロフェッショナルな設計において、操作対象のプロジェクトは必ず `MSProject.Project` 型の変数 に明示的にセットしなければならない。
一度オブジェクト変数にキャプチャしてしまえば、その後ユーザーがどのウィンドウをアクティブにしようが、VBAの内部では対象のプロジェクトを正確に保持し続けることができる。
プロジェクトを特定する3つのアプローチ
1. ファイルを開いた瞬間に参照を掴む(推奨)
2. ファイル名でコレクションから検索する
3. `ThisProject` を活用する(アドインやマクロ有効プロジェクトの場合)
—
3. プロダクション・コード:安全なプロジェクト操作の雛形
以下に、実務でそのまま転用できる、堅牢性を極めたコード例を示す。単にファイルを開くだけでなく、既に開いている場合のハンドリングも網羅している。
‘—————————————————————————————
‘ Procedure : SafeProjectOperation
‘ Author : チーフアーキテクト
‘ Purpose : 複数のプロジェクトが開かれた環境でも、対象を誤らずに操作する
‘—————————————————————————————
Public Sub SafeProjectOperation()
Dim targetProj As MSProject.Project
Dim filePath As String
Dim fileName As String
‘ 操作対象のフルパスを指定(実際は設定ファイルやDBから取得すべきだ)
filePath = “C:\Projects\Construction_Phase1.mpp”
fileName = “Construction_Phase1.mpp” ‘ ファイル名部分のみ
On Error GoTo ErrorHandler
‘ 1. 対象プロジェクトが既に開いているか確認
Set targetProj = GetProjectByName(fileName)
‘ 2. 開いていない場合は、FileOpenExを使用して開き、その瞬間に参照を掴む
If targetProj Is Nothing Then
‘ Application.FileOpenEx は成功すると対象を ActiveProject にするが、
‘ すかさず変数に代入することで、その後の「Active」依存を断つ
If Application.FileOpenEx(Name:=filePath, ReadOnly:=False) Then
Set targetProj = Application.ActiveProject
Else
Err.Raise vbObjectError + 1001, , “ファイルのオープンに失敗しました。”
End If
End If
‘ — ここから先は targetProj 変数のみを使用する —
‘ ユーザーが画面をどう操作しても、このコードは揺るがない
Dim t As MSProject.Task
Set t = targetProj.Tasks.Add(Name:=”API連携による自動生成タスク”)
t.Start = Date
t.Duration = “5d”
Debug.Print “プロジェクト ‘” & targetProj.Name & “‘ の更新が完了しました。”
CleanUp:
Set targetProj = Nothing
Exit Sub
ErrorHandler:
MsgBox “エラー発生: ” & Err.Description, vbCritical
Resume CleanUp
End Sub
‘— ヘルパー関数: プロジェクト名からオブジェクトを特定 —
Private Function GetProjectByName(ByVal projName As String) As MSProject.Project
Dim p As MSProject.Project
For Each p In Application.Projects
‘ 大文字小文字を区別せずに比較
If LCase(p.Name) = LCase(projName) Then
Set GetProjectByName = p
Exit Function
End If
Next p
Set GetProjectByName = Nothing
End Function
—
4. データベース・外部システム連携時の注意点
Project VBAをデータベースやAPIと連携させる際、さらに注意すべきライフサイクルの問題がある。
ファイルパスの正規化
`Project.Name` は通常ファイル名(拡張子付き)を返すが、環境によってはフルパスであったり、Project Server(PWA)環境ではURLであったりする。連携ツールを作る際は、`Project.FullName` と `Project.Name` の違いを明確に意識し、一意性を保証するロジックを組むこと。
非同期処理の罠
外部APIのレスポンスを待っている間に、ユーザーが別のプロジェクトを操作する可能性は非常に高い。上記のコードのように、プロシージャの開始直後に対象をオブジェクト変数に固定しておくことは、非同期的な挙動(イベントハンドラ内での処理など)においても生命線となる。
—
5. アーキテクトからの助言:保守性を高めるために
最後に、この記事を読んでいる君に伝えておきたい。
「動けばいい」コードは、技術的負債としていつか君自身の首を絞める。
1. `With ActiveProject` を極力排除せよ:
コードが短くなるメリットよりも、誤操作のデメリットが勝る。
2. 引数として `Project` オブジェクトを渡せ:
サブプロシージャを作る際は、`Sub UpdateTask(ByRef targetProj As Project)` のように、必ず対象を引数で明示する設計にせよ。
3. プロジェクトの「死」を意識せよ:
操作中に他の誰か(またはユーザー)がプロジェクトを閉じる可能性がある。長期実行されるマクロなら、処理の節目で `If targetProj Is Nothing` のチェックを入れるのが一流の仕事だ。
`Application.ActiveProject` という甘い誘惑を断ち切り、オブジェクトを厳密に制御すること。それが、Project VBAを真に掌握し、プロフェッショナルな自動化ツールを作り上げるための第一歩だ。
