Visio VBAを本気で業務システムや自動化パイプラインに組み込むとき、最初に直面する絶望は「オブジェクトモデルの独特なクセ」と「ファイルI/Oの脆弱性」だ。
特に、図面編集時のクラッシュや誤操作によるデータ破損は、業務において致命傷となる。一般的な `ActiveDocument.Save` に頼っているようでは、プロのエンジニアとは言えない。上書き保存の裏側で、確実にタイムスタンプと実行者を刻んだバックアップ世代管理を行う――今回は、実務の現場でそのまま稼働できる、堅牢性極まる `Document.SaveAs` のラッパー実装を伝授する。
—
なぜ素の `SaveAs` では業務に耐えられないのか?
多くの開発者は、次のようなコードを平気で書く。
‘ 【アンチパターン】これでは実務で破綻する
ActiveDocument.SaveAs “C:\Work\Drawing.vsdm”
このアプローチが非効率かつ危険な理由は以下の3点に集約される。
1. 上書きによる不可逆性: ユーザーが誤った変更を保存してしまった場合、過去の状態に戻す手段がない。
2. メタ情報の欠落: 「いつ、誰がその時点のファイルに手を加えたか」がファイル名や内部プロパティに担保されていないため、後から監査やバージョンの特定ができない。
3. 例外ハンドリングの欠如: 保存先のフォルダが存在しない、あるいは別プロセスがファイルを開いている(排他制御エラー)場合に、容赦なくVBAの実行時エラーでフリーズする。
プロのエンジニアが目指すべきは、「メインの保存を安全に行いつつ、アトミック(不可分)にタイムスタンプ付きのバックアップを別階層へ生成する仕組み」の構築である。
—
堅牢な世代管理バックアップ・アーキテクチャ
今回構築するラッパー関数の設計思想はこうだ。
- 保存先の自動検証: バックアップディレクトリが存在しない場合は、コード側で自動生成する(`MkDir` の多重階層エラー対策を含む)。
- 一意なファイル名の生成: `YYYYMMDD_HHMMSS_UserName_元のファイル名.vsdm` という命名規則により、ソート順がそのまま時系列の世代管理となるようにする。
- マスターへの影響排除: バックアップ処理で万が一エラーが発生しても、本来の図面保存処理(メインの `SaveAs`)を阻害しない、あるいは適切にロールバック・ロギングする設計にする。
—
プロダクションコード:`SaveWithBackup` モジュール
以下のコードをVisioの標準モジュール(例: `ModBackupSave`)にそのまま貼り付けてほしい。実務でのメンテナンス性を考慮し、エラーハンドリングと定数定義を徹底している。
Option Explicit
‘ ==============================================================================
‘ モジュール名: ModBackupSave
‘ 概要: タイムスタンプと実行者名を付与した世代管理バックアップ付き保存ラッパー
‘ ==============================================================================
‘ バックアップを格納するルートディレクトリ(環境に合わせて変更してください)
Private Const BACKUP_ROOT_PATH As String = “C:\VisioBackup\”
Public Sub ExecuteSafeSaveWithBackup()
Dim docTarget As Visio.Document
Set docTarget = ActiveDocument
‘ 1. ドキュメントが一度も保存されておらず、パスを持たない場合のガード
If docTarget.Path = “” Then
MsgBox “この図面はまだ一度も保存されていません。” & vbCrLf & _
“まずは通常の「名前を付けて保存」を手動で行ってください。”, vbExclamation, “保存エラー”
Exit Sub
End If
On Error GoTo ErrorHandler
‘ 2. メインファイルの保存(上書き保存、または最新状態の同期)
docTarget.Save
‘ 3. バックアップ生成処理の実行
Call CreateTimestampedBackup(docTarget)
MsgBox “図面の保存とバックアップの生成が正常に完了しました。”, vbInformation, “保存成功”
Exit Sub
ErrorHandler:
MsgBox “保存処理中に予期せぬエラーが発生しました。” & vbCrLf & _
“エラー番号: ” & Err.Number & vbCrLf & _
“内容: ” & Err.Description, vbCritical, “致命的エラー”
End Sub
Private Sub CreateTimestampedBackup(ByVal targetDoc As Visio.Document)
Dim originalPath As String
Dim originalName As String
Dim ext As String
Dim backupDir As String
Dim timeStamp As String
Dim userName As String
Dim backupFullPath As String
originalPath = targetDoc.Path ‘ 末尾のバックスラッシュを含む
originalName = targetDoc.Name
‘ 拡張子を分離(例: .vsdm)
Dim dotPos As Long
dotPos = InStrRev(originalName, “.”)
If dotPos > 0 Then
ext = Mid(originalName, dotPos)
originalName = Left(originalName, dotPos – 1)
Else
ext = “.vsdm”
End If
‘ タイムスタンプの生成 (YYYYMMDD_HHMMSS)
timeStamp = Format(Now, “yyyymmdd_hhnnss”)
‘ 実行者名の取得(環境変数から取得。取得できない場合は “Unknown”)
userName = Environ(“USERNAME”)
If userName = “” Then userName = “SystemUser”
‘ バックアップ保存先ディレクトリの構築(図面ごとのフォルダに分ける設計)
backupDir = BACKUP_ROOT_PATH & originalName & “\”
Call EnsureDirectoryExists(backupDir)
‘ バックアップファイルのフルパス作成
‘ 命名規則: タイムスタンプ_ユーザー名_元ファイル名.拡張子
backupFullPath = backupDir & timeStamp & “_” & userName & “_” & originalName & ext
‘ 4. ドキュメントの複製を別名で保存(これがバックアップの実体となる)
‘ ※ SaveAsEx を使うことで、ファイル形式の互換性やプレビュー保存などを制御可能
targetDoc.SaveAsEx backupFullPath, visSaveAsNoShrink
Debug.Print “Backup created successfully: ” & backupFullPath
End Sub
Private Sub EnsureDirectoryExists(ByVal dirPath As String)
‘ 階層ディレクトリを再帰的に作成する堅牢な関数
If Right(dirPath, 1) <> “\” Then dirPath = dirPath & “\”
If Len(Dir(dirPath, vbDirectory)) = 0 Then
‘ 親フォルダが存在しない場合を考慮し、VBAのMkDirで生成
‘ ※実運用ではFileSystemObjectを使うアプローチも有効
Dim parentPath As String
parentPath = Left(dirPath, InStrRev(Left(dirPath, Len(dirPath) – 1), “\”))
If Len(Dir(parentPath, vbDirectory)) = 0 Then
EnsureDirectoryExists parentPath
End If
MkDir dirPath
End If
End Sub
—
チーフアーキテクトが教える実装上の急所(Tips)
1. なぜ `ActiveDocument.Save` の後に `SaveAsEx` を使うのか?
バックアップを作るために元のドキュメントを別の場所に保存(`SaveAs`)すると、現在Visioでアクティブなドキュメントの参照先がバックアップファイル側に切り替わってしまうというVisio特有の悪夢のような仕様がある。
これを防ぐため、メインの保存を完了させた後に、メモリ上のドキュメントを汚さずに(あるいは影響を最小限に抑えて)複製を吐き出す必要がある。上記のコードでは `SaveAsEx` を用いて、元の作業用ドキュメントの状態を完全に維持したままバックアップファイルを生成している。
2. ディレクトリ構造のベストプラクティス
バックアップファイルをすべて同一フォルダに平置きすると、数ヶ月でファイル数が数千を超え、OSのI/Oパフォーマンスが著しく低下する。
上記のコードでは、`BACKUP_ROOT_PATH & originalName & “\” ` のように、図面名ごとのサブディレクトリを動的に掘る設計にしている。これにより、プロジェクト単位・図面単位での世代管理が美しく保たれる。
3. 古いバックアップの自動クリーンアップ(発展形)
世代管理の宿命として、放置すればストレージがパンクする。プロダクション環境では、上記の `CreateTimestampedBackup` の末尾に「30日以上経過したバックアップファイルを自動削除するFSO(FileSystemObject)のロジック」を組み込むと、完全にメンテナンスフリーな要塞システムが完成するだろう。
—
結論
業務の自動化やツール開発において、「動けばいい」というコードは技術的負債の温床にすぎない。
今回提供した `SaveWithBackup` ラッパーは、ユーザーのうっかりミスや環境起因のトラブルからデータを守るための「防壁」となる。あなたの開発するVisioソリューションにこの設計を取り入れ、ワンランク上の堅牢性を実現してほしい。
