【実務・中級編】【中級者向け】読み込み専用で開いたファイルの「名前を付けて保存」による上書き防止策 – Project VBA解析バイブル

スポンサーリンク

Project VBA堅牢化の極意:読み込み専用からの「名前を付けて保存」でデータ保全を徹底する

諸君、開発現場の最前線で業務効率化ツールと格闘する同志たちよ。
私は長年、Project VBAの深淵を覗き、その挙動を掌握してきたチーフアーキテクトだ。
今日は、一見すると些細に思えるが、実は企業の事業継続性を左右しかねない「ファイル運用」における極めて重要なテーマについて語ろう。

「既存ファイルを読み込み専用で開き、編集結果を別名で保存する」――このシンプルな運用フローを自動化する際、諸君は本当に堅牢な設計を施せているだろうか?
「読み込み専用で開けば、誤って上書きする心配はない」と安易に考えてはいないか?
もしそうであれば、それは致命的な誤解だ。本稿では、その甘い認識を打ち砕き、Project VBAのAPIの深い理解に基づいた、真に盤石なファイル保全戦略を伝授する。

データ保全の第一歩:なぜ「読み込み専用」だけでは不十分なのか?

「読み込み専用でファイルを開けば、元のデータは安全だ」――これは多くのアプリケーションで通用する常識であり、VBA開発者もそう信じがちだ。しかし、諸君が相手にしているのはMicrosoft Projectだ。その挙動には、他のOffice製品とは異なる独特の癖があることを認識しなければならない。

たしかに、`Application.FileOpenEx`メソッドで`ReadOnly:=True`を指定すれば、ProjectのGUI上では「読み取り専用」として開かれ、通常の上書き保存はできない。しかし、これはGUIレベルでの制約に過ぎない。もしユーザーが「ファイル」→「名前を付けて保存」を選択した場合、あるいはVBAコードが`Project.SaveAs`メソッドを呼び出した場合、読み込み専用の制約は容易に回避され、別のファイル名で保存されてしまう。

問題はここからだ。もしこの「名前を付けて保存」が、誤って元のファイルパスを指してしまったり、あるいは既存の重要なバックアップファイルを上書きしてしまったりしたらどうなるか? 想像してほしい。軽微な誤操作が、過去の履歴データや、現在進行中のプロジェクトの「唯一の真実」を破壊する引き金となりかねないのだ。

我々が目指すべきは、単なる「上書き防止」ではない。「どのような状況下でも、意図しないデータ破損を許さない」という、極限のデータ保全思想だ。そのためには、Project VBAのAPIを深く理解し、その挙動を完全に制御する必要がある。

堅牢な設計思想:チーフアーキテクトが説く3つの要諦

バグの起きない堅牢なツールを開発するには、コードを書く前に、その背後にある設計思想を確立することが不可欠だ。

1. ProjectオブジェクトのライフサイクルとAPIの意図を掌握せよ

Project VBAにおけるファイル操作は、`Application`オブジェクト、`Projects`コレクション、そして個々の`Project`オブジェクトの連携によって行われる。

  • `Application.FileOpenEx`: これは単なるファイルを開くメソッドではない。Projectアプリケーションのコンテキストで、新しい`Project`オブジェクトを生成し、`Projects`コレクションに追加する操作だ。`ReadOnly`引数は、この`Project`オブジェクトのプロパティとして設定され、GUIの振る舞いに影響を与える。
  • `Project.SaveAs`: 既存の`Project`オブジェクトの状態を、指定されたパスに新しいファイルとして永続化する。このメソッドは、`ReadOnly`の状態を無視して実行されるため、我々が介入すべきポイントとなる。
  • `Project.Saved`プロパティ: これが肝だ。開いている`Project`オブジェクトに変更があったかどうかを示すブール値。このプロパティこそが、保存処理のトリガーを判断する唯一無二の指標となる。

APIの引数一つ一つには、設計者の意図が込められている。`Merge`引数や`AddToMRU`引数など、細かな設定を適切に使いこなすことで、予期せぬ挙動を防ぎ、ユーザーエクスペリエンスを向上させることができる。

2. エラーハンドリングは「リカバリ戦略」として構築せよ

`On Error GoTo ErrorHandler`は、もはや最低限の作法に過ぎない。真の堅牢性とは、エラーが発生した際に、システムをいかに安全な状態に復帰させるか、ユーザーにどのような情報を提供し、次の行動を促すか、というリカバリ戦略に他ならない。

  • ファイルパスの検証: 指定されたファイルが存在するか? フォルダは存在するか? パスに不正な文字は含まれていないか? FSO (FileSystemObject) を活用し、事前に徹底的に検証せべし。
  • アクセス権の確認: 読み込み・書き込み権限があるか? ネットワークパスであれば、接続は確立されているか?
  • ディスク容量の確認: 保存先のディスク容量が不足していないか?
  • ユーザーへのフィードバック: エラー発生時には、何が起きたのか、どうすれば解決できるのかを具体的に伝える。抽象的なエラーメッセージはユーザーを混乱させるだけだ。

3. パフォーマンスとUXを両立せよ

自動化ツールは高速でなければ意味がない。しかし、ユーザーを置き去りにしてはならない。

  • 画面更新の抑制: `Application.ScreenUpdating = False`と`Application.DisplayAlerts = False`は、ファイルI/Oのような重い処理の前後で必ず設定せよ。これにより、無用な画面のちらつきや確認ダイアログ表示を抑制し、処理速度とユーザーエクスペリエンスを劇的に向上させる。ただし、エラー発生時には忘れずに`True`に戻すこと。
  • 無用なダイアログの排除: `Application.DisplayAlerts = False`は、保存時の「変更を保存しますか?」のような確認ダイアログを抑制する。これにより、自動化処理が途中で止まることを防ぐ。しかし、重要な判断を伴う場面では、適切なメッセージボックスでユーザーに判断を委ねるべきだ。

実装アプローチ:別名保存を強制するプロシージャの構築

それでは、具体的なコード例を見ていこう。ここでは、以下の要件を満たすプロシージャを構築する。

1. 指定されたファイルを読み込み専用で開く。
2. 開いたプロジェクトに変更が加えられたかを判断する。
3. 変更があった場合、自動的に元のファイル名にタイムスタンプを付加した別名で保存する。
4. ユーザーに保存結果を明確に通知する。
5. 堅牢なエラーハンドリングとパス操作を実装する。

Option Explicit

‘ ==============================================================================
‘ Sub: OpenAndSaveAsVersioned
‘ 目的: 既存のProjectファイルを読み込み専用で開き、変更があった場合に
‘ 元のファイル名にタイムスタンプを付加した別名で保存することを強制します。
‘ これにより、元データの意図しない上書きを確実に防止し、
‘ 簡易的なバージョン管理も実現します。
‘ ==============================================================================
Sub OpenAndSaveAsVersioned()
‘ 定数定義: 元プロジェクトファイルのパス
‘ ★★★ 環境に合わせてこのパスを修正してください ★★★
Const ORIGINAL_FILE_PATH As String = “C:\ProjectData\MyCriticalProject.mpp”

Dim pj As Project ‘ 開いたProjectオブジェクト
Dim fso As Object ‘ FileSystemObjectのインスタンス
Dim newFilePath As String ‘ 新しい保存先ファイルパス
Dim originalFileName As String ‘ 元ファイルのファイル名(拡張子なし)
Dim originalFileExt As String ‘ 元ファイルの拡張子

‘ ————————————————————————–
‘ パフォーマンスとユーザーエクスペリエンス向上のための初期設定
‘ ————————————————————————–
Application.ScreenUpdating = False ‘ 画面更新を抑制
Application.DisplayAlerts = False ‘ アラート表示を抑制(保存確認ダイアログなどを防ぐ)

‘ エラーハンドリングの開始
On Error GoTo ErrorHandler

‘ FileSystemObjectを生成
Set fso = CreateObject(“Scripting.FileSystemObject”)

‘ ————————————————————————–
‘ 事前チェック: 元ファイルの存在確認
‘ ————————————————————————–
If Not fso.FileExists(ORIGINAL_FILE_PATH) Then
MsgBox “指定されたプロジェクトファイルが見つかりません。” & vbCrLf & _
“パスを確認してください: ” & ORIGINAL_FILE_PATH, vbCritical, “エラー”
GoTo CleanExit ‘ エラー発生時は即座に終了処理へ
End If

‘ ————————————————————————–
‘ プロジェクトを読み込み専用で開く
‘ ————————————————————————–
‘ ReadOnly:=True: 読み込み専用で開くことを明示。GUIからの上書き保存を防止。
‘ Merge:=False: 他のユーザーが同時に開いている場合でもマージを試みず、排他的に開くことを試みる。
‘ これにより、共有環境での意図しない競合を防ぐ。
‘ AddToMRU:=False: 「最近使ったファイル」リストにこのファイルを追加しない。
‘ 自動処理で頻繁に開くファイルがリストを占有するのを防ぐ。
Set pj = Application.FileOpenEx( _
Name:=ORIGINAL_FILE_PATH, _
ReadOnly:=True, _
Merge:=False, _
AddToMRU:=False _
)

‘ ————————————————————————–
‘ ここで、開いたプロジェクトに対して自動処理やユーザーによる編集が行われます。
‘ 例:
‘ pj.Tasks(1).Name = “フェーズ1 – 初期分析 (改訂版)”
‘ pj.Resources.Add Name:=”新規リソース”, Type:=pjResourceTypeWork
‘ pj.SetBaselines mpBaseline, True ‘ ベースライン設定も変更とみなされる
‘ ————————————————————————–
‘ 例として、ベースラインを設定してみる
If pj.Baselines.Count = 0 Then
pj.SetBaselines mpBaseline, True ‘ ベースラインがなければ設定
MsgBox “ベースラインを設定しました。プロジェクトに変更があります。”, vbInformation
Else
‘ 既存のベースラインを変更しない場合は、SavedプロパティはFalseにならない
‘ ここで意図的に変更を加える例
pj.ProjectSummaryTask.Name = pj.ProjectSummaryTask.Name & ” (更新)”
End If

‘ ————————————————————————–
‘ 変更の有無を確認し、必要であれば別名で保存
‘ ————————————————————————–
If Not pj.Saved Then ‘ pj.Saved が False の場合、プロジェクトに変更がある
‘ 新しい保存パスを生成 (例: MyCriticalProject_20231027_103045.mpp)
originalFileName = fso.GetFileName(ORIGINAL_FILE_PATH) ‘ “MyCriticalProject.mpp”
originalFileExt = fso.GetExtensionName(originalFileName) ‘ “mpp”
originalFileName = fso.GetBaseName(originalFileName) ‘ “MyCriticalProject”

‘ タイムスタンプ形式: YYYYMMDD_HHMMSS
Dim timestamp As String
timestamp = Format(Now, “yyyymmdd_hhmmss”)

‘ 新しいファイル名を構築
newFilePath = fso.BuildPath( _
fso.GetParentFolderName(ORIGINAL_FILE_PATH), _
originalFileName & “_” & timestamp & “.” & originalFileExt _
)

‘ 保存先フォルダの存在チェックと作成 (もし存在しない場合)
If Not fso.FolderExists(fso.GetParentFolderName(newFilePath)) Then
On Error Resume Next ‘ フォルダ作成エラーはリカバリ可能かもしれないので一時的にエラーを無視
fso.CreateFolder fso.GetParentFolderName(newFilePath)
On Error GoTo ErrorHandler ‘ エラーハンドラを再度有効化
If Err.Number <> 0 Then
MsgBox “保存先フォルダの作成に失敗しました: ” & fso.GetParentFolderName(newFilePath) & vbCrLf & _
“エラー: ” & Err.Description, vbCritical, “エラー”
GoTo CleanExit
End If
End If

‘ 名前を付けて保存
‘ FileFormat:=pj.FileFormat: 元のファイル形式を維持
‘ ReadOnlyRecommended:=False: 新規保存なので「読み取り専用を推奨」は不要。念のため明示。
‘ AddToMRU:=False: この新しいファイルを「最近使ったファイル」に追加しない。
pj.SaveAs _
Name:=newFilePath, _
FileFormat:=pj.FileFormat, _
ReadOnlyRecommended:=False, _
AddToMRU:=False

MsgBox “プロジェクトに変更があったため、新しいファイル名で保存されました。” & vbCrLf & _
“保存先: ” & newFilePath, vbInformation, “保存完了”
Else
MsgBox “プロジェクトに変更はありませんでした。”, vbInformation, “変更なし”
End If

CleanExit:
‘ ————————————————————————–
‘ 終了処理: 開いたプロジェクトを閉じ、オブジェクトを解放
‘ ————————————————————————–
If Not pj Is Nothing Then
‘ SaveChanges:=pj.Saved がポイント。
‘ プロジェクトが既に保存済み (pj.Saved = True) なら、変更を保存せずに閉じる。
‘ 未保存 (pj.Saved = False) なら、変更を破棄して閉じる。
‘ ここでは SaveAs 済みなので pj.Saved は True になっているはず。
pj.Close SaveChanges:=pj.Saved
End If

Set pj = Nothing
Set fso = Nothing

‘ 画面更新とアラート表示を元に戻す
Application.ScreenUpdating = True
Application.DisplayAlerts = True
Exit Sub

ErrorHandler:
‘ 予期せぬエラー発生時の処理
MsgBox “致命的なエラーが発生しました。” & vbCrLf & _
“エラー番号: ” & Err.Number & vbCrLf & _
“説明: ” & Err.Description, vbCritical, “システムエラー”
Resume CleanExit ‘ 終了処理へジャンプ
End Sub

‘ ==============================================================================
‘ Sub: SetProjectBaselineExample
‘ 目的: プロジェクトにベースラインを設定する例 (OpenAndSaveAsVersioned から呼び出す想定)
‘ このプロシージャを呼び出すことで、プロジェクトの Saved プロパティが False になる。
‘ ==============================================================================
Sub SetProjectBaselineExample(ByRef targetProject As Project)
On Error GoTo ErrorHandler

If Not targetProject Is Nothing Then
‘ ベースラインを設定する
‘ mpBaseline: デフォルトのベースライン (Baseline0)
‘ True: 全タスクのベースラインを設定
targetProject.SetBaselines mpBaseline, True
Debug.Print “プロジェクト ‘” & targetProject.Name & “‘ にベースラインを設定しました。”
Else
Debug.Print “プロジェクトオブジェクトが Nothing です。ベースラインを設定できません。”
End If
Exit Sub

ErrorHandler:
Debug.Print “SetProjectBaselineExample でエラーが発生しました: ” & Err.Description
End Sub

コード解説と設計のポイント

  • `Application.FileOpenEx`の徹底活用:
  • `ReadOnly:=True`でGUIからの上書き保存をブロックしつつ、我々のVBAコードからは`SaveAs`を許可する、というVBAの特性を理解した利用法。
  • `Merge:=False`で、複数のユーザーがファイルを編集しようとした際の競合を抑制。これはProjectのファイル形式がXMLベースであることからくる特性で、安易なマージはデータ破壊を招きかねない。
  • `AddToMRU:=False`で、自動化用途のファイルが「最近使ったファイル」リストを占有するのを防ぐ。これは細部にわたるUXへの配慮だ。
  • `pj.Saved`プロパティの絶対的信頼:
  • `If Not pj.Saved Then`こそが、変更の有無を判断し、別名保存をトリガーする最も信頼性の高い方法だ。ProjectのAPIが提供するこのプロパティを最大限に活用せよ。
  • FileSystemObject (FSO) によるパス操作:
  • `CreateObject(“Scripting.FileSystemObject”)`でFSOのインスタンスを取得し、パスの結合、ファイル/フォルダの存在チェック、拡張子の取得などを確実に行う。文字列連結によるパス生成はバグの温床となるため、FSOの使用を徹底せよ。
  • `fso.BuildPath`でOSのパス区切り文字の違いを吸収し、`fso.GetParentFolderName`で親フォルダパスを確実に取得する。
  • タイムスタンプによるユニークなファイル名生成:
  • `Format(Now, “yyyymmdd_hhmmss”)`で生成されるタイムスタンプは、ファイル名を一意にし、簡易的なバージョン管理を可能にする。これにより、過去の任意の時点のデータを復元できる基盤が作られる。
  • エラーハンドリングの具体化:
  • ファイルが見つからない、保存先フォルダがない、といった具体的なエラーシナリオを想定し、それぞれに対して適切なメッセージと終了処理を用意している。`On Error GoTo`だけでなく、エラー発生時の「リカバリ」を意識した記述だ。
  • 終了処理の徹底:
  • `pj.Close SaveChanges:=pj.Saved`は、`pj.Saved`プロパティの状態に応じて適切にプロジェクトを閉じる賢い記述だ。変更がなければ保存せずに閉じ、変更があった(かつ`SaveAs`済みで`pj.Saved`が`True`になっている)場合は、その状態のまま閉じる。
  • オブジェクトの解放 (`Set pj = Nothing`, `Set fso = Nothing`) は、メモリリークを防ぎ、リソースを適切に管理するための必須項目だ。
  • 保守性の高いコード構造:
  • メインプロシージャから分離された`SetProjectBaselineExample`のようなヘルパープロシージャを定義することで、各機能の単一責任原則を守り、コードの可読性と再利用性を高めている。

ファイル・データベース連携における注意点

この「名前を付けて保存」戦略を、より広範なシステムに組み込む際には、以下の点に留意する必要がある。

1. ネットワークパスと共有環境の現実

  • UNCパスの利用: ネットワーク共有上のファイルにアクセスする場合、`\\ServerName\ShareName\FolderName\FileName.mpp`のようなUNCパスを常に使用すること。ドライブレターはユーザー環境によって異なる可能性があり、信頼性に欠ける。
  • 競合の回避: Projectファイルは本質的にマルチユーザー編集には向いていない。`Application.FileOpenEx`の`Merge`引数で部分的に回避はできるが、真の共同作業を求めるなら、Project Server/OnlineやSharePointとの連携を検討すべきだ。VBAレベルでの複雑な排他制御は、バグとパフォーマンスの問題の温床となる。

2. データベースとの連携:MPPファイルの直接保存は厳禁

  • MPPファイルの直接保存は避ける: データベースにProjectファイルをバイナリとして直接保存する設計は、長期的に見て悪手だ。Projectファイルの内部構造は複雑であり、バージョンアップで変更される可能性がある。また、データベースの肥大化、検索性の低下、個別のデータ要素へのアクセス困難など、多くの問題を引き起こす。
  • Project APIを活用したデータ連携: データベースにProjectデータを保存する場合、`Project`オブジェクトのAPI(`Tasks`, `Resources`, `Assignments`コレクションなど)を使用して、必要なデータを抽出し、リレーショナルデータベースのテーブルに格納すべきだ。これにより、データの一貫性、検索性、レポーティングの容易さが格段に向上する。
  • バージョン管理の徹底: データベース側で、Projectデータのバージョン管理を実装することも検討せよ。例えば、`ProjectID`と`RevisionNumber`を複合主キーとして、各タスクやリソースのデータを管理する。

保守性向上のためのチーフアーキテクトからの金言

最後に、諸君が今後作成する自動化ツールが、長期にわたって現場で活躍するための、保守性向上のためのヒントを与えよう。

  • 定数 (Const) の活用: マジックナンバーや固定のパス、メッセージ文字列などは、必ず定数として宣言せよ。これにより、変更の際の一点修正が可能となり、可読性も向上する。
  • プロシージャ/関数の分割: 各プロシージャ/関数は「単一の責任」を持つべきだ。ファイルパスの生成、存在チェック、プロジェクトの保存、メッセージ表示など、機能を細かく分割することで、各コンポーネントのテストが容易になり、全体の複雑性が低減する。
  • 意味のあるコメント: 「何をしているか」だけでなく、「なぜそうするのか」を明確に記述せよ。特にAPIの特定の引数を選んだ理由や、エラーハンドリングの意図は、将来の保守者がコードを理解する上で不可欠だ。
  • 命名規則の統一: 変数、プロシージャ、関数の命名には、プロジェクト内で統一された規則を適用せよ。これにより、コードの予測可能性が高まり、可読性が向上する。

まとめ:データ保全は自動化の最上位要件である

今日のテーマは、単なる「読み込み専用ファイルからの名前を付けて保存」というテクニカルな話に留まらない。それは、データという企業の最も貴重な資産を、いかにして不慮の事故から守り抜くかという、開発プロジェクトにおける最上位の要件に関わる問題だ。

VBAによる自動化は、業務効率を飛躍的に向上させる強力な武器となる。しかし、その力は、使い方を誤ればデータ破壊という凶器にもなり得る。真のプロフェッショナルとは、目先の効率化だけでなく、潜在的なリスクを徹底的に洗い出し、その対策を堅牢な設計と実装で実現できる者を指す。

今回示したコードは、その一例に過ぎない。この設計思想を自身のプロジェクトに適用し、Project VBAのAPIの深い理解と、データ保全への揺るぎないコミットメントをもって、現場に真の価値をもたらす自動化ツールを構築してほしい。

諸君の挑戦を期待する。

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