【テクニカル・上級編】【上級】Visioアドイン開発を見据えたステンシル管理クラスの設計 – Visio VBA解析バイブル

スポンサーリンク
スポンサーリンク

Visio VBAの深淵:ステンシル管理をクラス化し、エンタープライズ級の堅牢性を手に入れる

Visioを単なる「作図ツール」として使っているうちはいい。だが、それを「自動作図エンジン」として、あるいは社内標準の「設計自動化プラットフォーム」として運用しようとした瞬間、多くのエンジニアはステンシル管理という泥沼に突き落とされる。

適当に `Documents.AddStencil` を呼び出し、`Masters(“Name”).Drop` でシェイプを配置する。そんな素人のコードを書いていては、ステンシルの数が増えた瞬間にメモリ消費が跳ね上がり、ファイルパスの不整合でランタイムエラーが頻発し、最終的にはVisioごとクラッシュする。

プロフェッショナルが構築すべきは、ステンシルのライフサイクルを完全に制御下に置いた「ステンシル管理クラス」だ。本稿では、COMオブジェクトの挙動とメモリ最適化を突き詰めた、究極のステンシル管理アーキテクチャを提示する。

1. 凡庸な実装と、アーキテクトの実装の決定的な差

多くの開発者が書くコードは、手続き型だ。
「ステンシルを開く」→「マスターを探す」→「配置する」という流れを、あちこちのサブプロシージャに散りばめる。これは最悪だ。ステンシルファイルが移動しただけで全コードを修正しなければならないし、同じステンシルを二重にロードしてリソースを浪費する。

真のアーキテクチャは、「ステンシルが現在開いているか」という状態を隠蔽し、クライアント側には「マスター名を指定して配置する」というインターフェースのみを公開する。

設計の要諦:

1. 遅延ロード(Lazy Loading): 必要な時に初めてステンシルを読み込む。
2. キャッシュ機構: 一度ロードしたステンシルは内部的に保持し、再検索コストをゼロにする。
3. カプセル化: ファイルパスの解決ロジックをクラス内部に閉じ込め、外部からはマスター名だけでアクセスさせる。
4. 明示的なリソース解放: COMオブジェクトの参照カウントを制御し、ゾンビプロセス化を防ぐ。

2. 実装:`StencilManager` クラスの極限設計

以下に、私が推奨する `StencilManager` クラスの実装を示す。ここでは `Scripting.Dictionary` を用いてロード済みステンシルをキャッシュし、高速なアクセスを実現している。

クラスモジュール:`clsStencilManager`

' 参照設定: Microsoft Scripting Runtime が必要
Option Explicit

Private m_stencils As Scripting.Dictionary ' ロード済みステンシルのキャッシュ
Private m_stencilPathRoot As String        ' ステンシル格納ルートパス

' --- 初期化 ---
Public Sub Initialize(ByVal rootPath As String)
    Set m_stencils = New Scripting.Dictionary
    m_stencilPathRoot = rootPath
End Sub

' --- マスターシェイプの配置 (メインエントリ) ---
' @param stencilName ステンシルファイル名 (例: "NetworkShapes.vssx")
' @param masterName  マスターシェイプ名
' @param targetPage  配置先のページ
Public Function DropMaster(ByVal stencilName As String, ByVal masterName As String, ByRef targetPage As Visio.Page) As Visio.Shape
    Dim vStencil As Visio.Stencil
    Dim vMaster As Visio.Master
    
    On Error GoTo ErrorHandler

    ' 1. キャッシュ確認とロード
    Set vStencil = GetOrLoadStencil(stencilName)
    
    ' 2. マスターの存在確認と取得
    ' Mastersコレクションへの直接アクセスは重いため、エラーハンドリングで制御
    On Error Resume Next
    Set vMaster = vStencil.Masters(masterName)
    On Error GoTo ErrorHandler
    
    If vMaster Is Nothing Then
        Err.Raise vbObjectError + 513, "clsStencilManager", "マスター '" & masterName & "' がステンシル '" & stencilName & "' 内に見つかりません。"
    End If
    
    ' 3. 配置 (Drop)
    ' 座標指定なしでDropした場合、原点(0,0)に配置される
    Set DropMaster = vMaster.Drop(targetPage)
    
    Exit Function

ErrorHandler:
    ' ログ出力などのエラー処理をここに実装
    Err.Raise Err.Number, "clsStencilManager.DropMaster", Err.Description
End Function

' --- ステンシルのロード管理 (内部メソッド) ---
Private Function GetOrLoadStencil(ByVal stencilName As String) As Visio.Stencil
    Dim vStencil As Visio.Stencil
    Dim fullPath As String
    
    ' キャッシュに存在すればそれを返す
    If m_stencils.Exists(stencilName) Then
        Set vStencil = m_stencils(stencilName)
        ' 念のため、Visio側で意図せず閉じられていないかチェック
        If Not vStencil Is Nothing Then
            Set GetOrLoadStencil = vStencil
            Exit Function
        End If
    End If
    
    ' パス構築と存在確認
    fullPath = m_stencilPathRoot & "\" & stencilName
    If Not FileExistsAPI(fullPath) Then
        Err.Raise vbObjectError + 514, "clsStencilManager", "ステンシルファイルが見つかりません: " & fullPath
    End If
    
    ' ステンシルのロード (読み取り専用で開くのが鉄則)
    Set vStencil = Visio.Application.Documents.AddStencil(fullPath, Visio.VisOpenReadOnly)
    
    ' キャッシュに登録
    m_stencils.Add stencilName, vStencil
    
    Set GetOrLoadStencil = vStencil
End Function

' --- Windows API による高速なファイル存在確認 ---
' VBAのDir関数はネットワークパスや特殊文字で不安定なためAPIを使用する
Private Declare PtrSafe Function GetFileAttributes Lib "kernel32" Alias "GetFileAttributesA" (ByVal lpFileName As String) As Long
Private Const FILE_ATTRIBUTE_READONLY = &H1

Private Function FileExistsAPI(ByVal filePath As String) As Boolean
    Dim attrs As Long
    attrs = GetFileAttributes(filePath)
    FileExistsAPI = (attrs <> -1) ' -1 (INVALID_FILE_ATTRIBUTES) でなければ存在
End Function

' --- リソースの明示的解放 ---
Public Sub Terminate()
    Dim key As Variant
    For Each key In m_stencils.Keys
        ' ステンシルを閉じる(必要に応じて)
        ' Visio.Application.Documents.Close m_stencils(key)
        Set m_stencils(key) = Nothing
    Next key
    Set m_stencils = Nothing
End Sub

3. チーフアーキテクトの視点:ここが「極限」のポイント

① Windows APIによるファイルチェックの必然性

VBA標準の `Dir()` 関数は、UNCパス(`\\Server\Share\…`)や非常に長いパスを扱う際に予期せぬ挙動を示すことがある。エンタープライズ環境では、ステンシルはファイルサーバーに集約されるのが常識だ。`GetFileAttributes` APIを叩くことで、OSレベルの正確かつ高速な存在確認を実現している。

② `VisOpenReadOnly` の徹底

ステンシルを `VisOpenReadOnly` で開くことは、単なる安全策ではない。書き込み権限で開いた場合、複数のユーザーが同時にツールを起動した際にファイルロックが発生し、システム全体が停止する。自動化ツールにおいて、マスターの変更は管理者が行うものであり、ランタイムで変更することは禁忌である。

③ COMオブジェクトのライフサイクルとメモリリーク

Visio VBAにおいて最も危険なのは、`Stencil` や `Master` オブジェクトの参照を保持しすぎることだ。特にループ内で大量にシェイプを配置する場合、明示的に `Set object = Nothing` を行わないと、メモリ上の参照カウントが適切に減らず、Visioの動作が次第に重くなる。
`Terminate` メソッドを実装し、アドインの終了時やクラスの破棄時に辞書の中身をクリアすることで、メモリの断片化を最小限に抑えている。

4. 実践的な利用シーン

このクラスを導入することで、メインのロジックは驚くほどシンプルになる。

Public Sub GenerateSystemArchitecture()
    Dim mgr As clsStencilManager
    Dim pg As Visio.Page
    Dim shp As Visio.Shape
    
    Set pg = Visio.ActivePage
    Set mgr = New clsStencilManager
    
    ' ルートパスを指定して初期化
    mgr.Initialize "C:\Corporate\VisioStencils"
    
    On Error Resume Next
    ' 複雑なパス指定やロード処理を意識せず、マスター名だけで配置できる
    Set shp = mgr.DropMaster("ServerIcons.vssx", "Web_Server_01", pg)
    If Err.Number <> 0 Then
        MsgBox "配置失敗: " & Err.Description
    End If
    On Error GoTo 0
    
    ' 最後にリソースを解放
    mgr.Terminate
    Set mgr = Nothing
End Sub

5. 結論:保守性の正体とは

多くのエンジニアが「コードを短くすること」を保守性と勘違いしている。しかし、真の保守性とは「変更の影響範囲が限定的であること」だ。

ステンシルの保存場所が変わったか? $\rightarrow$ `Initialize` の引数を変えるだけ。
マスター名が変更されたか? $\rightarrow$ 呼び出し側の文字列を変えるだけ。
メモリリークが発生しているか? $\rightarrow$ `clsStencilManager` の `Terminate` 処理を最適化するだけ。

このように、関心事を分離し、低レイヤーのCOM操作をクラス内部に封じ込めること。これこそが、レガシーなVBA環境においても、現代的なソフトウェアエンジニアリングを適用する唯一の道である。

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