マクロの記録を卒業せよ。CSVから制約条件を流し込む「制約制御エンジン」の構築術
こんにちは。現場の自動化を支えるアーキテクトです。
「マクロの記録」機能を使って、なんとか作業を自動化した。そこまでは素晴らしい一歩です。しかし、実務で直面する「データの変動」という荒波を乗り越えるには、記録されたコードだけでは力不足であることを、あなたも感じ始めているのではないでしょうか?
例えば、プロジェクトのスケジュール管理。
「タスクAを必ず〇月〇日に開始させる(Must Start On)」「タスクBは〇月〇日以降に開始する(Start No Earlier Than)」といった制約条件(Constraints)を、数百、数千のタスクに対して手入力していませんか?
もし、その制約条件がExcelやCSVで管理されているなら、私たちは「一つずつ操作を記録する」のではなく、「データを読み込み、Projectのオブジェクトへ適切に流し込む『エンジン』」を作るべきなのです。
今日は、外部データとProjectを高度に連携させる、実務中級レベルのテクニックを伝授します。ここをマスターすれば、あなたは「マクロを使う人」から「システムを構築するエンジニア」へと進化できますよ。
1. なぜ「タスク名」ではなく「ID」で紐づけるのか?
設計思想において、最も重要な概念の一つが「一意識別子(Unique Identifier)」です。
初心者の多くは、「タスク名」をキーにしてデータを紐づけようとします。しかし、これは非常に危険な設計です。
- 「設計検討」というタスクが、プロジェクト内に2つ存在するかもしれない。
- 誰かが誤ってタスク名を「設計検討(修正)」と書き換えた瞬間、プログラムは迷子になる。
一方、Task IDは、Projectのオブジェクトモデルにおいて、そのタスクを指し示す絶対的な住所です。外部のCSVからデータをインポートする際は、必ずこの「ID」をキーとして扱う。これが、堅牢な自動化エンジンを構築するための鉄則です。
2. インポート・データの設計(マッピングテーブル)
まずは、読み込むCSVの構造を決めましょう。ここでは、人間が読みやすい言葉(文字列)と、Projectが理解できる値(Enum/列挙型)を橋渡しするための「マッピング」の考え方を取り入れます。
【CSVファイル構成案: `constraints_import.csv`】
| TaskID | ConstraintType | ConstraintDate |
|---|---|---|
| 5 | Must Start On | 2023/10/01 |
| 12 | Start No Earlier Than | 2023/10/05 |
| 20 | Finish No Later Than | 2023/10/15 |
| 45 | As Soon As Possible |
ここで重要なのは、`ConstraintType` には `Must Start On` のような「人間が理解できる言葉」を入れ、それをVBA側でProjectの内部定数(`pjTaskConstraintType`)に変換するという点です。
3. 実装:制約条件インポート・エンジン
それでは、魂を込めたコードを見ていきましょう。単に動くだけでなく、エラーに強く、保守しやすい構造を目指します。
Option Explicit
''' <summary>
''' CSVファイルから制約条件を読み込み、Projectタスクに適用するメインエンジン
''' </summary>
Sub ImportConstraintsFromCSV()
Dim csvPath As String
Dim fileNo As Integer
Dim lineData As String
Dim dataArray() As String
Dim targetTask As Task
Dim taskId As Long
Dim constraintStr As String
Dim constraintDate As Variant
Dim targetConstraintType As PjTaskConstraintType
' --- 設定エリア ---
' 読み込むCSVのパスを指定(実際はファイル選択ダイアログ等にするとより実用的です)
csvPath = ThisWorkbook.Path & "\constraints_import.csv"
' ------------------
' ファイルの存在確認
If Dir(csvPath) = "" Then
MsgBox "CSVファイルが見つかりません: " & csvPath, vbCritical
Exit Sub
End If
' 画面更新を停止してパフォーマンスを向上させる
Application.ScreenUpdating = False
On Error GoTo ErrorHandler
fileNo = FreeFile
Open csvPath For Input As #fileNo
' ヘッダー行をスキップ
If Not EOF(fileNo) Then Line Input #fileNo, lineData
' CSVの各行をループ処理
Do While Not EOF(fileNo)
Line Input #fileNo, lineData
' カンマ区切りで分割(簡易的なCSVパース)
dataArray = Split(lineData, ",")
' 配列の要素数が足りているかチェック
If UBound(dataArray) >= 2 Then
taskId = CLng(Trim(dataArray(0)))
constraintStr = Trim(dataArray(1))
constraintDate = Trim(dataArray(2))
' 1. タスクの特定(IDによる検索)
Set targetTask = Nothing
On Error Resume Next
Set targetTask = ActiveProject.Tasks(taskId)
On Error GoTo ErrorHandler
If Not targetTask Is Nothing Then
' 2. 文字列からProject定数への変換(マッピング処理)
targetConstraintType = MapConstraintString(constraintStr)
' 3. 制約条件の適用
targetTask.ConstraintType = targetConstraintType
' 日付が指定されている場合のみ適用
If constraintStr <> "As Soon As Possible" And constraintStr <> "As Late As Possible" Then
If IsDate(constraintDate) Then
targetTask.ConstraintDate = CDate(constraintDate)
End If
End If
Debug.Print "Success: Task ID " & taskId & " applied " & constraintStr
Else
Debug.Print "Warning: Task ID " & taskId & " not found."
End If
End If
Loop
Close #fileNo
Application.ScreenUpdating = True
MsgBox "インポート処理が完了しました。", vbInformation
Exit Sub
ErrorHandler:
Close #fileNo
Application.ScreenUpdating = True
MsgBox "予期せぬエラーが発生しました。" & vbCrLf & _
"Error Number: " & Err.Number & vbCrLf & _
"Description: " & Err.Description, vbCritical
End Sub
''' <summary>
''' 人間が入力した文字列を、Project VBAの制約タイプ定数に変換する
''' </summary>
Private Function MapConstraintString(ByVal strType As String) As PjTaskConstraintType
Select Case strType
Case "Must Start On"
MapConstraintString = pjTaskConstraintMustStartOn
Case "Start No Earlier Than"
MapConstraintString = pjTaskConstraintStartNoEarlierThan
Case "Finish No Later Than"
MapConstraintString = pjTaskConstraintFinishNoLaterThan
Case "As Soon As Possible"
MapConstraintString = pjTaskConstraintNone
Case "As Late As Possible"
' ASAP/ALAPの制御は制約タイプではなく、スケジューリングのロジックに近いが、
' ここでは便宜上、制約なし(None)として扱うか、特定の制約を割り当てる
MapConstraintString = pjTaskConstraintNone
Case Else
' 不明な文字列が来た場合は「制約なし」とする安全策
MapConstraintString = pjTaskConstraintNone
End Select
End Function
4. プロの視点:ここが「極限の知見」への分かれ道
このコードをただコピペして終わりにするのは、もったいない。実務で「プロ」として評価されるための、3つの重要ポイントを解説します。
① `Select Case` によるマッピングの抽象化
上記のコードで、`MapConstraintString` という関数を独立させた理由に注目してください。
もし将来、「新しい制約条件のルールを追加したい」となったとき、メインのロジック(CSVを読み込む部分)を一切触らずに、この関数を書き換えるだけで対応できます。これを「関心の分離」と呼びます。
② オブジェクトの存在確認(Nothingチェック)
`Set targetTask = ActiveProject.Tasks(taskId)` を実行した際、もしそのIDが存在しないと、VBAは即座に実行時エラーで停止します。
実務のデータには必ず「ゴミ」が混じります。`If Not targetTask Is Nothing Then` という一文を入れるだけで、プログラムの堅牢性は劇的に向上します。
③ パフォーマンスと副作用の制御
大量のタスクを処理する場合、`Application.ScreenUpdating = False` は必須です。画面の描画を止めることで、処理速度は数倍から数十倍に跳ね上がります。
また、エラーが発生した際に `Close #fileNo` を忘れてしまうと、ファイルがロックされたままになり、次の操作ができなくなるという「初歩的だが致命的なミス」を招きます。`ErrorHandler` で確実にファイルを閉じる設計にしています。
5. まとめ:次のステップへ
今回構築したエンジンは、単なる「インポートツール」ではありません。「外部データ(CSV)を、Projectのオブジェクトモデルへと翻訳するインターフェース」です。
この考え方が身につけば、以下のような発展も容易になります。
- CSVだけでなく、データベース(SQL Server)から直接制約を読み込む。
- 適用した制約条件をログファイルとして出力し、変更履歴を残す。
- 制約条件が矛盾している(例:開始日より終了日が前)場合に警告を出す。
「マクロの記録」という殻を破り、データとオブジェクトの関係性を意識したプログラミングへ。
ここをクリアすれば、あなたのProject VBAのスキルは、間違いなく次のステージへ到達しています。
頑張ってください。応援していますよ!
