【テクニカル・上級編】【実務直結】ページ削除時の「誤削除防止」ダイアログと、削除ログを外部テキストに出力する安全策 – Visio VBA解析バイブル

スポンサーリンク

Visio VBAを掌握する極限の知見:ページ誤削除を防ぐ防衛的プログラミングと監査ログ実装の全技術

VisioのVBA開発において、最も避けなければならない事故の一つが「重要ページの誤削除」である。
Visioのオブジェクトモデルにおいて、`Page.Delete`メソッドは容赦がない。Undoバッファの制限や、マルチページにわたる複雑なリンク構造を持つ図面において、一歩の誤りが取り返しのつかないデータ損失を招く。

本稿では、単なるコードのスニペットの提示にとどまらない。Windows APIを駆使した堅牢なモーダルダイアログ制御、VBAのメモリ管理の闇に切り込むオブジェクトのライフサイクル最適化、そして企業コンプライアンスに耐えうる外部テキストへの監査ログ出力メカニズムを統合した「実務直結型の防衛的VBAアーキテクチャ」を解説する。

—

1. Visioページ削除におけるアーキテクチャ上の罠

Visioの`Pages`コレクションからの削除は、単に配列から要素を取り除くのとは訳が違う。

  • 暗黙の再インデックス化: ページを削除すると、それ以降のページのインデックス番号(Index)が動的にずれる。ループ内でページを順次削除する場合、末尾から前方に向かって処理(逆順ループ)しなければ、必ず「Subscript out of range(インデックスが有効範囲にありません)」エラーに直面する。
  • 図形リソースの解放遅延: ページが保持する数千の`Shape`オブジェクト、それらを結ぶコネクタ、マスターシェイプの参照カウントが適切にデクリメントされない場合、Visioのプロセス内にメモリリークが蓄積する。

これらを制御下に入れ、さらに「人間のうっかりミス」をシステム側で完全にブロックするための設計が求められる。

—

2. 実装コード:防衛的ページ削除&監査ログエンジン

以下のコードは、実務の現場で即座に投入可能なプロダクション品質のVBAモジュールである。
安全確認のためのWindows APIによる確実なプロンプト制御と、UTF-8(BOM付き)でのファイルI/Oによる監査ログ出力を実装している。

Option Explicit

‘ ==============================================================================
‘ モジュール名: ModPageSecurityGuard
‘ 概要: ページ削除時の誤削除防止ダイアグラムおよび操作監査ログ出力
‘ ==============================================================================

‘ Windows API: 独自の堅牢なメッセージボックス制御(最前面表示・警告アイコン)
If VBA7 Then
Private Declare PtrSafe Function MessageBoxW Lib “user32” ( _
ByVal hWnd As LongPtr, _
ByVal lpText As LongPtr, _
ByVal lpCaption As LongPtr, _
ByVal uType As Long) As Long
Else
Private Declare Function MessageBoxW Lib “user32″ ( _
ByVal hWnd As Long, _
ByVal lpText As Long, _
ByVal lpCaption As Long, _
ByVal uType As Long) As Long
If

‘ メッセージボックスの定数定義
Private Const MB_ICONWARNING As Long = &H30&
Private Const MB_YESNO As Long = &H4&
Private Const MB_DEFBUTTON2 As Long = &H100& ‘ デフォルトで「いいえ」を選択(誤爆防止の極意)
Private Const IDYES As Long = 6

”’

”’ 指定したページを安全に削除し、監査ログを記録する
”’

”’ 削除対象のページ名 Public Sub SafeDeletePageWithAudit(ByVal targetPageName As String)
Dim vsoPage As Visio.Page
Dim targetIndex As Long
Dim logFilePath As String

‘ 1. オブジェクトの特定と存在確認
Set vsoPage = GetPageByName(targetPageName)
If vsoPage Is Nothing Then
Call WriteAuditLog(targetPageName, “FAILED”, “指定されたページが見つかりません。”)
MsgBox “ページ ‘” & targetPageName & “‘ が見つかりませんでした。”, vbCritical, “エラー”
Exit Sub
End If

‘ 2. バックグラウンドページや保護されたページの除外判定
If vsoPage.Background = True Then
MsgBox “バックグラウンドページは直接削除できません。”, vbExclamation, “操作拒否”
Set vsoPage = Nothing
Exit Sub
End If

‘ 3. Windows APIによる厳重な確認ダイアログ(デフォルト「いいえ」)
Dim promptMsg As String
promptMsg = “警告: ページ ‘” & targetPageName & “‘ を完全に削除しようとしています。” & vbCrLf & _
“この操作は元に戻せません。本当に削除しますか?”

Dim apiResult As Long
#If VBA7 Then
Dim hwndParent As LongPtr
#If
hwndParent = Application.WindowHandle
#Else
Dim hwndParent As Long
hwndParent = Application.WindowHandle
#End If

apiResult = MessageBoxW(hwndParent, StrPtr(promptMsg), StrPtr(“【Visio 慎重な操作確認】”), MB_ICONWARNING Or MB_YESNO Or MB_DEFBUTTON2)

If apiResult <> IDYES Then
Call WriteAuditLog(targetPageName, “ABORTED”, “ユーザーによって削除がキャンセルされました。”)
MsgBox “ページの削除を中止しました。”, vbInformation, “キャンセル”
Set vsoPage = Nothing
Exit Sub
End If

‘ 4. トランザクション処理と削除実行
Dim pageNameBackup As String
pageNameBackup = vsoPage.Name

On Error GoTo ErrorHandler

‘ VisioのUndoスコープを開始(万が一のエラー時にロールバック可能にする)
Dim undoScopeID As Long
undoScopeID = Application.BeginUndoScope(“Secure Page Deletion”)

‘ ページ削除の実行
vsoPage.Delete

Application.EndUndoScope undoScopeID, True

‘ 5. 監査ログの出力(成功)
Call WriteAuditLog(pageNameBackup, “SUCCESS”, “正常に削除されました。”)

‘ 6. メモリの明示的解放
Set vsoPage = Nothing
Exit Sub

ErrorHandler:
‘ エラー時のUndoロールバック
Application.EndUndoScope undoScopeID, False

Dim errDesc As String
errDesc = Err.Description
Call WriteAuditLog(pageNameBackup, “ERROR”, “削除処理中に異常終了: ” & errDesc)

Set vsoPage = Nothing
MsgBox “ページ削除中に予期せぬエラーが発生しました。” & vbCrLf & errDesc, vbCritical, “致命的エラー”
End Sub

”’

”’ ページ名からVisio.Pageオブジェクトを安全に取得する
”’

Private Function GetPageByName(ByVal pageName As String) As Visio.Page
Dim vsoPages As Visio.Pages
Dim vsoPage As Visio.Page
Dim i As Long

Set vsoPages = ActiveDocument.Pages
For i = 1 To vsoPages.Count
Set vsoPage = vsoPages(i)
If vsoPage.Name = pageName Then
Set GetPageByName = vsoPage
Exit Function
End If
‘ ループ内のCOMオブジェクト参照を明示的にクリア(メモリ肥大化防止)
Set vsoPage = Nothing
Next i

Set GetPageByName = Nothing
End Function

”’

”’ 操作履歴を外部テキスト(TSV形式)に記録する
”’

Private Sub WriteAuditLog(ByVal pageName As String, ByVal status As String, ByVal details As String)
Dim fso As Object
Dim ts As Object
Dim logDir As String
Dim logFile As String

‘ ログ保存先はアドイン稼働フォルダ、またはドキュメントと同一階層を推奨
logDir = ActiveDocument.Path
If logDir = “” Then
logDir = Environ(“USERPROFILE”) & “\Documents” ‘ 未保存ドキュメントのフォールバック
End If

logFile = logDir & “\Visio_Audit_Log.txt”

Set fso = CreateObject(“Scripting.FileSystemObject”)

‘ ファイルが存在しない場合はヘッダー行を書き込む
Dim isNewFile As Boolean
isNewFile = Not fso.FileExists(logFile)

‘ ForAppending (8), Unicode (-1 = UTF-16, 但しここではADODB等を使わず簡易的にANSI/UTF-8制御)
‘ 実務では文字化けを防ぐためADODB.Streamを用いるのがベストプラクティス
Dim stream As Object
Set stream = CreateObject(“ADODB.Stream”)
stream.Type = 2 ‘ adTypeText
stream.Charset = “UTF-8”

If fso.FileExists(logFile) Then
stream.Open
stream.LoadFromFile logFile
stream.Position = stream.Size ‘ 末尾へ移動
Else
stream.Open
stream.WriteText “Timestamp” & vbTab & “User” & vbTab & “Machine” & vbTab & “Document” & vbTab & “TargetPage” & vbTab & “Status” & vbTab & “Details” & vbCrLf
End If

Dim logRow As String
logRow = Format(Now, “yyyy-mm-dd hh:nn:ss”) & vbTab & _
Environ(“USERNAME”) & vbTab & _
Environ(“COMPUTERNAME”) & vbTab & _
ActiveDocument.Name & vbTab & _
pageName & vbTab & _
status & vbTab & _
details

stream.WriteText logRow & vbCrLf
stream.SaveToFile logFile, 2 ‘ adSaveCreateOverWrite
stream.Close

Set stream = Nothing
Set ts = Nothing
Set fso = Nothing
End Sub

—

3. チーフアーキテクトが解説するコードの急所

A. デフォルトボタンの罠を回避する Windows API 活用

標準の `MsgBox` 関数では、どのボタンにフォーカス(デフォルト)を当てるかの制御が貧弱であり、ユーザーがキーボードの「Enter」を無意識に叩いた瞬間に重要ページが吹き飛ぶ事故が後を絶たない。
本実装では `MessageBoxW` API に対し `MB_DEFBUTTON2`(第2ボタン=「いいえ」にフォーカス)を明示的に指定している。これにより、ダイアログが出て即座にEnterを押下しても、物理的に「いいえ」が選択される鉄壁の防御を実現している。

B. COMオブジェクトのライフサイクルとメモリ最適化

VBAにおける最大の悪習は、ループ内でCOMオブジェクトを変数に代入し続け、スコープを抜けるまで解放しないことである。Visio VBAではこれが積み重なると、COMプロセスのヒープ領域を圧迫し、最終的に「OutOfMemory」やVisio自体の強制終了を引き起こす。
`GetPageByName` 関数内の `Set vsoPage = Nothing` は、単なるおまじないではなく、COMインターフェイスの参照カウント(Reference Counting)を即時デクリメントし、ガベージコレクションの負荷を軽減するための必須の儀式である。

C. ADODB.Streamによる堅牢なログ出力

社内監査の要件を満たすため、ログファイルは単なるテキスト出力ではなく、マルチバイト文字(日本語のページ名やユーザー名)が文字化けしないよう `ADODB.Stream` を用いて強制的に UTF-8 で書き込んでいる。さらに、タブ区切り(TSV形式)を採用することで、後続のExcelやPower BIによる自動集計・分析を容易にしている。

—

4. 現場への導入と運用上の注意点

1. VBAプロジェクトのデジタル署名:
企業内のセキュリティポリシーが厳格化されている環境では、マクロが無効化される。自己証明書または企業内CA発行の証明書でVBAプロジェクトに署名し、信頼できる発行元として登録すること。
2. ログファイルの競合対策:
複数ユーザーが同一ネットワーク共有上の図面を同時に操作する場合、ログファイルの書き込み競合(Permission Denied)が発生する可能性がある。ローカル環境への出力、あるいはDB連携への拡張を視野に入れた設計変更を推奨する。

真にプロフェッショナルなVBAコードとは、華麗なアルゴリズムのことではない。「人間は必ず間違える」という前提に立ち、システムがそのミスを優しく、かつ厳格に包み込む防衛的設計(Defensive Design)が実装されているか否か、それだけである。

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