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

スポンサーリンク

Project VBAの深淵: 読み込み専用ファイル操作における絶対的安全策と『名前を付けて保存』の罠

長年、エンタープライズの最前線でProject VBAシステムを構築・保守してきた者として、幾度となくデータ破損の危機と向き合ってきました。その中でも、最も頻繁に、そして巧妙にシステム運用者の意図を裏切るのが「ファイルの読み込み専用オープン」と「名前を付けて保存」の挙動です。表面的なVBAの記述だけでは見抜けない、OSレベル、アプリケーションレベルの深い挙動を理解せずして、真に堅牢なシステムは構築できません。

この記事では、Projectファイルを読み込み専用で開き、その編集結果を確実に別名で保存することで、元のデータへの意図しない上書きを完全に防止する、極限の知見を伝授します。単なるVBAのメソッド呼び出しに留まらず、Windows APIの介入、オブジェクトのライフサイクル、そしてメモリ管理に至るまで、その真髄を深掘りしていきましょう。

導入: 「読み込み専用」がもたらす偽りの安心

ProjectファイルをVBAで操作する際、多くの開発者がまず考えるのが、`Application.FileOpen`や`Application.FileOpenEx`メソッドの`ReadOnly`引数を`True`に設定することでしょう。

Application.FileOpen Name:=”C:\Projects\Original.mpp”, ReadOnly:=True

この記述は一見、安全な運用を約束するように見えます。ユーザーがProjectアプリケーション上でこのファイルを開くと、確かにタイトルバーには「(読み取り専用)」と表示され、保存しようとすれば「名前を付けて保存」ダイアログが強制的に開きます。しかし、これはあくまでユーザーインターフェース上の挙動に過ぎません。VBAのコード内で`ActiveProject.Save`や`ActiveProject.SaveAs`を呼び出した場合、この「読み取り専用」の制約は、我々が期待するほど絶対的なものではないのです。

ここが問題の核心です。Projectアプリケーションの内部的なファイルハンドル管理、そしてOSのファイルシステムとのインタラクションにおいて、VBAの`ReadOnly`引数だけではカバーしきれない脆弱性が存在します。 特に、誤った`SaveAs`の呼び出し方や、複雑なエラーハンドリングの欠如は、元のファイルを意図せず上書きしてしまう危険性をはらんでいます。シニアエンジニアやシステム管理者にとって、このリスクは致命的であり、単なるコーディングミスでは片付けられない、設計思想レベルの問題として捉えるべきです。

問題の核心: `Project.SaveAs`とOSの乖離

Project VBAにおけるファイル操作の最も危険な側面は、`Project.SaveAs`メソッドの挙動にあります。

`Application.FileOpenEx Name:=sSourceFilePath, ReadOnly:=True`
このコードで開かれたプロジェクトは、Projectアプリケーションのセッション内では「読み取り専用」の状態として扱われます。これは、UIからの保存操作においては「名前を付けて保存」を強制するトリガーとなります。

しかし、VBA内部で以下のようなコードを実行した場合を想像してください。

Dim proj As Project
‘ … 読み込み専用で開く処理 …
Set proj = Application.ActiveProject

‘ 誤った運用例:
proj.SaveAs Name:=”C:\Projects\Original.mpp” ‘ !!! 同じパスを指定 !!!

このコードが実行された場合、Projectアプリケーションは元のファイルパスに対して「名前を付けて保存」を試みます。多くのVBA開発者は、読み込み専用で開いているのだから、この操作は「許可されない」または「エラーになる」と期待するかもしれません。しかし、VBAの`SaveAs`メソッドは、多くの場合、このUI上の「読み込み専用」フラグを無視して、ファイルシステムへの書き込みを試みます。

なぜこのような挙動になるのでしょうか?
Projectアプリケーションは、ファイルを開く際にOSからファイルハンドルを取得します。`ReadOnly:=True`は、このハンドルを読み取りモードで取得しようとしますが、アプリケーション内部のロジックが、VBAからの`SaveAs`呼び出しに対して、新しいハンドルを書き込みモードで取得し直す、あるいは既存のハンドルを昇格させる、といった挙動を取る可能性があります。特に、元のファイルがOSレベルで書き込み可能であった場合、`SaveAs`は「新しいファイルを作成する」というより「既存ファイルを指定された内容で置き換える」操作として解釈され、警告なしに上書きが行われてしまうリスクがあるのです。

このリスクを完全に排除するためには、VBAのアプリケーションレベルの制御だけでなく、OSのファイルシステムレベルでの介入が必要です。

【極限の知見】絶対的安全を担保する多層防御戦略

我々が目指すのは、いかなるシナリオにおいても元のデータが破損しない、絶対的な安全です。これを実現するためには、単一のVBAメソッドに依存するのではなく、以下の多層防御戦略を組み合わせる必要があります。

第一層: VBAによるファイルオープン時の制御と保存パスの強制

VBAでProjectファイルを開く際、`ReadOnly`引数を指定することは当然ですが、それに加えて、保存時には必ず新しいパスを指定させる運用を徹底します。

‘ VBAによるファイルオープン。ReadOnlyは必須だが、これだけでは不十分。
Application.FileOpenEx Name:=sSourceFilePath, ReadOnly:=True
Set proj = Application.ActiveProject

‘ ユーザーに新しい保存パスを強制するロジック
Dim sNewFilePath As String
sNewFilePath = Application.FileSaveAsEx(Name:=Application.ActiveProject.Name & “_Modified.mpp”, _
Type:=pjMPP, _
DefaultPath:=Application.ActiveProject.Path)
‘ もしユーザーがキャンセルした場合や、既存ファイルを選んだ場合の処理
If sNewFilePath = “” Then
‘ 処理中断、またはエラーハンドリング
proj.Close pjDoNotSave
Set proj = Nothing
Exit Function
End If

‘ 別名で保存。このSaveAsの前に、後述するAPIによる防御が不可欠。
proj.SaveAs Name:=sNewFilePath

この層はユーザーインターフェースの模倣に過ぎず、悪意ある、あるいは無知なVBAコードからの上書きを防ぐことはできません。真の防御は、次の層で実現します。

第二層: ファイルシステムレベルでの防御 (Windows API)

Projectアプリケーションがファイルハンドルをどのように扱うかに関わらず、OSレベルでファイルの書き込み属性を制御することで、物理的な上書きを防止します。これは、`SetFileAttributes`というWindows APIを呼び出すことで実現します。

戦略:
1. 元のProjectファイルを開く前に、一時的にそのファイルに「読み取り専用」属性を付与します。
2. Project VBAでファイルを読み込み専用で開きます。
3. 必要な編集を行い、新しいパスに「名前を付けて保存」します。この際、元のファイルはOSレベルで読み取り専用になっているため、`SaveAs`が意図せず元のファイルを指定した場合でも、OSが書き込みを拒否します。
4. 新しいファイルが保存された後、元のファイルの「読み取り専用」属性を解除し、元の状態に戻します。

これにより、VBAの`SaveAs`メソッドが例え内部的に上書きを試みたとしても、OSがその書き込みをブロックするため、元のデータは絶対的に保護されます。

Windows API宣言の注意点:
VBA7 (Office 2010以降) では、64ビット環境に対応するため`PtrSafe`キーワードが必要です。レガシー環境(Office 2007以前)との互換性を保つため、条件付きコンパイルディレクティブ(`#If VBA7`)を使用します。また、文字列をAPIに渡す際には、ポインタ渡しが安全です。

‘ Windows API宣言 (32bit/64bit対応)
‘ kernel32.dll からファイル属性を操作する関数を宣言
If VBA7 Then
Private Declare PtrSafe Function SetFileAttributes Lib “kernel32” Alias “SetFileAttributesA” ( _
ByVal lpFileName As String, ByVal dwFileAttributes As Long) As Long
Private Declare PtrSafe Function GetFileAttributes Lib “kernel32” Alias “GetFileAttributesA” ( _
ByVal lpFileName As String) As Long
Else
Private Declare Function SetFileAttributes Lib “kernel32” Alias “SetFileAttributesA” ( _
ByVal lpFileName As String, ByVal dwFileAttributes As Long) As Long
Private Declare Function GetFileAttributes Lib “kernel32” Alias “GetFileAttributesA” ( _
ByVal lpFileName As String) As Long
End If

‘ ファイル属性定数
Private Const FILE_ATTRIBUTE_NORMAL As Long = &H80 ‘ 通常ファイル属性
Private Const FILE_ATTRIBUTE_READONLY As Long = &H1 ‘ 読み取り専用属性
Private Const INVALID_FILE_ATTRIBUTES As Long = -1 ‘ ファイル属性取得失敗時の戻り値

第三層: オブジェクトのライフサイクルとメモリ管理

どんな堅牢なコードも、リソースリークやオブジェクトの不適切な管理があれば、最終的にはシステムを不安定にします。特にProjectのような重量級アプリケーションオブジェクトを扱うVBAでは、以下の原則を徹底すべきです。

  • 明示的な解放: `Set proj = Nothing`によるオブジェクトの解放は必須です。特に、複数のプロジェクトを短期間で開閉するようなシナリオでは、解放を怠るとメモリフットプリントが増大し、最終的には`OutOfMemory`エラーやパフォーマンスの著しい低下を招きます。
  • 適切なクローズ: `Project.Close`メソッドを使用し、変更を保存するか破棄するかを明示的に指定します(`pjDoNotSave`など)。`Application.Quit`はアプリケーション全体を終了させるため、乱用すべきではありません。
  • エラーハンドリングでの解放: `On Error GoTo`でエラーハンドラにジャンプした場合でも、開いたプロジェクトオブジェクトが確実に閉じられ、解放されるようロジックを組み込みます。これにより、予期せぬエラー発生時でもリソースが残留するのを防ぎます。
  • ガベージコレクションの理解: VBAにはC#のような高度なガベージコレクションはありません。COMオブジェクトへの参照が残存していれば、メモリは解放されません。したがって、明示的な`Set Nothing`はVBAにおけるデベロッパーの責務です。

実装例と解説: 多層防御を統合した堅牢なファイル操作

以下のコードは、上記の多層防御戦略を組み合わせたものです。レガシー環境から最新環境まで対応し、エラーハンドリングも考慮に入れています。

‘ Windows API宣言 (32bit/64bit対応)
‘ kernel32.dll からファイル属性を操作する関数を宣言
If VBA7 Then
Private Declare PtrSafe Function SetFileAttributes Lib “kernel32” Alias “SetFileAttributesA” ( _
ByVal lpFileName As String, ByVal dwFileAttributes As Long) As Long
Private Declare PtrSafe Function GetFileAttributes Lib “kernel32” Alias “GetFileAttributesA” ( _
ByVal lpFileName As String) As Long
Else
Private Declare Function SetFileAttributes Lib “kernel32” Alias “SetFileAttributesA” ( _
ByVal lpFileName As String, ByVal dwFileAttributes As Long) As Long
Private Declare Function GetFileAttributes Lib “kernel32” Alias “GetFileAttributesA” ( _
ByVal lpFileName As String) As Long
End If

‘ ファイル属性定数
Private Const FILE_ATTRIBUTE_NORMAL As Long = &H80 ‘ 通常ファイル属性
Private Const FILE_ATTRIBUTE_READONLY As Long = &H1 ‘ 読み取り専用属性
Private Const INVALID_FILE_ATTRIBUTES As Long = -1 ‘ ファイル属性取得失敗時の戻り値

”’

”’ 既存のProjectファイルを読み込み専用で開き、編集後に新しいファイル名で保存する。
”’ 元ファイルへの誤った上書きを防ぐため、Windows APIによるファイル属性操作も行います。
”’

”’ 開く元のProjectファイルのフルパス ”’ 編集結果を保存する新しいProjectファイルのフルパス ”’ 保存が成功した場合はTrue、失敗した場合はFalse
Public Function OpenProjectReadOnlyAndSaveAs(ByVal sSourceFilePath As String, ByVal sNewFilePath As String) As Boolean
Dim proj As Project ‘ 開かれたProjectオブジェクト
Dim lOriginalAttributes As Long ‘ 元ファイルの属性を保持
Dim bAttributesChanged As Boolean ‘ 元ファイルの属性を変更したかどうかのフラグ
Dim bOpened As Boolean ‘ Projectが正常に開かれたかどうかのフラグ
Dim bSaved As Boolean ‘ Projectが正常に保存されたかどうかのフラグ

On Error GoTo ErrorHandler ‘ エラーハンドラを設定

‘ — 1. 元ファイルの存在確認とパスの正規化 —
If Dir(sSourceFilePath, vbNormal) = “” Then
Debug.Print “エラー: 指定されたファイル ” & sSourceFilePath & ” が見つかりません。”
OpenProjectReadOnlyAndSaveAs = False
Exit Function
End If
sSourceFilePath = CreateObject(“Scripting.FileSystemObject”).GetAbsolutePathName(sSourceFilePath)

‘ — 2. 元ファイルの属性を取得し、一時的に読み取り専用に設定 —
‘ これがOSレベルでの上書き防止の要となる。
lOriginalAttributes = GetFileAttributes(sSourceFilePath)
If lOriginalAttributes = INVALID_FILE_ATTRIBUTES Then
Debug.Print “エラー: ファイル属性の取得に失敗しました: ” & sSourceFilePath
OpenProjectReadOnlyAndSaveAs = False
Exit Function
End End If

‘ 元ファイルが既に読み取り専用でなければ、一時的に設定
If Not (lOriginalAttributes And FILE_ATTRIBUTE_READONLY) Then
If SetFileAttributes(sSourceFilePath, lOriginalAttributes Or FILE_ATTRIBUTE_READONLY) = 0 Then
‘ 属性設定に失敗した場合でも続行は可能だが、上書き防止策は弱まるため警告
Debug.Print “警告: 元ファイルを一時的に読み取り専用に設定できませんでした: ” & sSourceFilePath & ” (ErrorCode: ” & Err.LastDllError & “)”
Else
bAttributesChanged = True ‘ 属性を変更したフラグを立てる
Debug.Print “情報: 元ファイルを一時的に読み取り専用に設定しました。”
End If
Else
Debug.Print “情報: 元ファイルは既に読み取り専用です。”
End If

‘ — 3. Project ファイルを読み込み専用で開く —
Application.FileOpenEx Name:=sSourceFilePath, ReadOnly:=True
Set proj = Application.ActiveProject ‘ 開かれたプロジェクトへの参照を取得
bOpened = True ‘ プロジェクトが正常に開かれたことを記録
Debug.Print “情報: プロジェクト ‘” & proj.Name & “‘ を読み込み専用で開きました。”

‘ — 4. ここでプロジェクトに対する変更処理を行う —
‘ 例: タスクの情報を取得する、リソースを追加する、ガントチャートを更新する、など
‘ この時点では、projオブジェクトは読み込み専用で開かれているが、VBAによる変更は可能。
‘ ただし、その変更はまだ保存されていない。
‘ Dim t As Task
‘ For Each t In proj.Tasks
‘ If Not t Is Nothing Then
‘ Debug.Print “タスク: ” & t.Name
‘ End If
‘ Next t

‘ — 5. 別名で保存 —
‘ ここで `proj.SaveAs Name:=sSourceFilePath` としても、
‘ OSレベルで元ファイルが読み取り専用のため、上書きはブロックされる。
‘ 常に新しいパスを指定することが推奨される。
proj.SaveAs Name:=sNewFilePath ‘ 新しいファイルパスで保存
bSaved = True ‘ 保存が成功したことを記録
Debug.Print “情報: プロジェクトを ‘” & sNewFilePath & “‘ として保存しました。”

‘ — 6. 元のプロジェクトを閉じる —
‘ 読み込み専用で開いているため、proj.Saved は通常 True のはずだが、念のため。
If Not proj Is Nothing Then
proj.Close pjDoNotSave ‘ 変更を保存せずに元のプロジェクトを閉じる
Set proj = Nothing ‘ オブジェクトの明示的解放
Debug.Print “情報: 元のプロジェクトを閉じ、オブジェクトを解放しました。”
End If

‘ — 7. 元ファイルの属性を元に戻す —
If bAttributesChanged Then
If SetFileAttributes(sSourceFilePath, lOriginalAttributes) = 0 Then
Debug.Print “警告: 元ファイルの属性を元に戻せませんでした: ” & sSourceFilePath & ” (ErrorCode: ” & Err.LastDllError & “)”
Else
Debug.Print “情報: 元ファイルの属性を元に戻しました。”
End If
End If

OpenProjectReadOnlyAndSaveAs = bSaved ‘ 最終的な保存結果を返す
Exit Function

ErrorHandler:
Debug.Print “実行時エラー: ” & Err.Number & ” – ” & Err.Description & ” @ ” & Now()

‘ エラー発生時に開いたプロジェクトを確実に閉じる
If bOpened And Not proj Is Nothing Then
On Error Resume Next ‘ Close中にさらにエラーが発生する可能性を考慮
proj.Close pjDoNotSave ‘ 保存せずに閉じることで、意図しない変更の残留を防ぐ
Set proj = Nothing
Debug.Print “情報: エラー発生により開いたプロジェクトを閉じ、オブジェクトを解放しました。”
End If

‘ 元ファイルの属性を元に戻す処理をエラーハンドラ内でも実行
If bAttributesChanged Then
On Error Resume Next ‘ 属性変更エラーが新たなエラーを発生させないように
If SetFileAttributes(sSourceFilePath, lOriginalAttributes) = 0 Then
Debug.Print “警告: エラーハンドラ内で元ファイルの属性を元に戻せませんでした: ” & sSourceFilePath & ” (ErrorCode: ” & Err.LastDllError & “)”
Else
Debug.Print “情報: エラーハンドラ内で元ファイルの属性を元に戻しました。”
End If
End If

OpenProjectReadOnlyAndSaveAs = False ‘ エラー時はFalseを返す
End Function

‘ — 使用例 —
Sub TestOpenAndSaveAsRobust()
Dim sOriginalPath As String
Dim sNewPath As String

‘ 既存のProjectファイルパスを指定してください
sOriginalPath = “C:\Temp\SampleProject.mpp”
‘ 新規作成または上書きされるProjectファイルパスを指定してください
sNewPath = “C:\Temp\SampleProject_Modified.mpp”

‘ テスト用のファイルがない場合は、Projectを起動し、新規プロジェクトを適当に保存して
‘ C:\Temp\SampleProject.mpp を作成してください。
‘ または、コード内でダミーファイルを作成するロジックを追加しても良いでしょう。

If OpenProjectReadOnlyAndSaveAs(sOriginalPath, sNewPath) Then
Debug.Print “— 処理成功: プロジェクトは安全に別名保存されました。 —”
Debug.Print “元のファイル: ” & sOriginalPath
Debug.Print “新規ファイル: ” & sNewPath
Else
Debug.Print “— 処理失敗: プロジェクトの別名保存に失敗しました。詳細をDebug.Printで確認してください。 —”
End If
End Sub

パフォーマンスとスケーラビリティへの考察

この多層防御戦略は、API呼び出しを伴うため、一般的なVBAコードよりもわずかにオーバーヘッドが増加します。しかし、`GetFileAttributes`や`SetFileAttributes`といったファイルシステムAPIは非常に軽量であり、その実行時間はミリ秒単位です。大規模なプロジェクトファイルであっても、ファイルI/O自体の時間に比べれば、このAPI呼び出しによる影響は無視できるレベルです。

I/Oのボトルネック: 真のパフォーマンスボトルネックは、Projectファイルのオープンと保存のI/O処理にあります。特にネットワークパス上の巨大なファイルを扱う場合、VBAの処理速度が問題になることは稀で、ネットワーク帯域やストレージ性能が支配的です。この点を理解し、可能であればローカルキャッシュの利用や、必要なデータのみを抽出する設計を検討すべきです。

システム間連携: この堅牢なファイル操作ロジックは、他のシステム(例: SharePoint、RDBMS)からトリガーされる自動化処理においても極めて有効です。一貫したデータ保全の原則は、システム連携の信頼性を飛躍的に向上させます。

レガシー環境と将来性

Project VBAは、その歴史の長さゆえ、Office 2003からMicrosoft 365の最新版に至るまで、様々なバージョンで稼働しています。このコードは、`PtrSafe`キーワードの条件付きコンパイルによって、比較的広い範囲のバージョンで動作するよう設計されています。

  • 互換性: Office 2007以前の32bit環境では`#Else`側の宣言が、Office 2010以降の64bit環境では`#If VBA7`側の宣言がそれぞれ適用されます。これにより、レガシー環境の保守と、最新環境への移行の両方を視野に入れたコードとして機能します。
  • VBAの限界と移行: VBAは依然として多くの企業で重要な役割を担っていますが、その機能やパフォーマンスには限界があります。複雑なビジネスロジックや大規模なデータ処理、高度なUI連携が必要な場合は、VB.NETやC#を用いたProject SDK (Project Interop) への移行を検討する時期が来るかもしれません。しかし、既存のVBA資産は膨大であり、その保守と漸進的な改善は、現実的なシステム管理者の責務です。本記事で提示したような、VBAの限界をOSレベルで補完する知見は、この移行期において極めて価値を持ちます。

結論: データ保全の哲学

Project VBAにおける「読み込み専用」ファイル操作と「名前を付けて保存」の挙動は、表面的な理解では見落としがちな深い罠を秘めています。単にVBAの`ReadOnly`引数を`True`にするだけでは、元のデータへの意図しない上書きを完全に防ぐことはできません。

我々が提供したのは、VBAのアプリケーションレベルの制御を超え、Windows APIを介してファイルシステムそのものに介入するという、多層的な防御戦略です。このアプローチにより、Projectアプリケーションの内部的な挙動や、ユーザーの誤操作、あるいは不適切なコード実行から、元のProjectファイルを絶対的に保護することが可能になります。

この知見は、単なるコードの羅列ではありません。それは、データ保全に対する揺るぎない哲学であり、長年の経験から培われたシステム設計の真髄です。あなたの管理するProject VBAシステムにこの多層防御を組み込むことで、データの整合性が確保され、運用リスクが最小化され、最終的には企業全体の信頼性向上に寄与するでしょう。これこそが、伝説的なチーフアーキテクトが追求する、極限の知見なのです。

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