Visio VBAを掌握する極限の知見:Shapeをラップした業務オブジェクト指向設計の極意
Visio VBAにおける開発は、往々にして「泥沼のプロシージャ記述」に陥りがちだ。`ActivePage.Shapes.Item(i)` のような直接参照を重ね、`CellsU(“Prop.Price”)` の文字列キーでプロパティを引くコードが散見される。規模が拡大するにつれ、メンテナンス不可能なスパゲッティコードと化すのは時間の問題だ。
プロフェッショナルなシステム開発において、生(生体)の `Shape` オブジェクトをそのまま業務ロジックに露出させるのは悪手である。本稿では、Visioの標準 `Shape` オブジェクトをVBAのクラスモジュールでラップし、型安全かつ堅牢な業務オブジェクト(Domain Object)を構築する極限の手法を解説する。
—
1. なぜVisio VBAでクラスモジュールを使うのか?
Visioのオブジェクトモデルはフラットであり、図形(Shape)は本質的に「描画のためのDOM」に過ぎない。これを業務上のエンティティ(例:サーバーラック、ネットワーク機器、製造ラインの工程)として扱うには、抽象化レイヤーが必要となる。
クラスモジュールによるラップが生み出す最大のメリットは以下の3点だ。
1. カプセル化と型安全性: `ShapeSheet` の冗長なセルアクセス(`CellsU` や `Prop.xxx`)を隠蔽し、意味のあるプロパティとメソッドを提供する。
2. ビジネスロジックの分離: 図形の見た目(座標、色)と業務データ(価格、型番、バリデーション状態)の関心を分離する。
3. メモリと参照の管理: 循環参照の防止と適切なオブジェクト解放(Lifetime Management)を担保する。
—
2. アーキテクチャ設計:ラップクラスの実装
実際に、ネットワーク機器を表す `DeviceShape` クラスを実装する。このクラスは、内部に本物の `Visio.Shape` を保持しつつ、業務固有のインターフェースを提供する。
クラスモジュール:`DeviceShape.cls`
VERSION 1.0 CLASS
BEGIN
MultiUse = -1 ‘True
END
Attribute VB_Name = “DeviceShape”
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
‘ ==============================================================================
‘ クラス名: DeviceShape
‘ 概要: Visio.Shapeをラップし、業務固有のプロパティと振る舞いを提供する
‘ ==============================================================================
Option Explicit
‘ 内部保持するVisioのネイティブシェイプ
Private m_InternalShape As Visio.Shape
‘ カスタムプロパティ用のカスタム定義キー
Private Const PROP_MODEL As String = “Prop.ModelNumber”
Private Const PROP_PRICE As String = “Prop.UnitPrice”
Private Const PROP_STATUS As String = “Prop.ValidationStatus”
‘ — イニシャライザとデストラクタ —
Public Sub Initialize(ByVal targetShape As Visio.Shape)
If targetShape Is Nothing Then
Err.Raise 91, “DeviceShape”, “ラップ対象のShapeが指定されていません。”
End If
Set m_InternalShape = targetShape
End Sub
Private Sub Class_Terminate()
‘ ガベージコレクションに頼らず、明示的に参照を切断
Set m_InternalShape = Nothing
End Sub
‘ — プロパティ(カプセル化されたShapeSheetアクセス) —
Public Property Get ID() As Long
ID = m_InternalShape.ID
End Property
Public Property Get Name() As String
Name = m_InternalShape.Name
End Property
Public Property Get ModelNumber() As String
On Error Resume Next
ModelNumber = m_InternalShape.CellsU(PROP_MODEL).ResultStr(“”)
If Err.Number <> 0 Then ModelNumber = “UNKNOWN”
On Error GoTo 0
End Property
Public Property Let ModelNumber(ByVal value As String)
EnsurePropertyExists PROP_MODEL, “型番”
m_InternalShape.CellsU(PROP_MODEL).FormulaU = Chr(34) & value & Chr(34)
End Property
Public Property Get UnitPrice() As Currency
On Error Resume Next
UnitPrice = m_InternalShape.CellsU(PROP_PRICE).ResultIU
If Err.Number <> 0 Then UnitPrice = 0
On Error GoTo 0
End Property
Public Property Let UnitPrice(ByVal value As Currency)
EnsurePropertyExists PROP_PRICE, “単価”
m_InternalShape.CellsU(PROP_PRICE).ResultIU = value
End Property
‘ — ビジネスロジック・メソッド —
Public Function Validate() As Boolean
‘ 業務ルールの検証:型番が未設定の場合はエラー状態にする
Dim isValid As Boolean
isValid = (Len(Trim(Me.ModelNumber)) > 0) And (Me.UnitPrice >= 0)
EnsurePropertyExists PROP_STATUS, “検証状態”
If isValid Then
m_InternalShape.CellsU(PROP_STATUS).FormulaU = “0” ‘ 0: 正常
m_InternalShape.Layer成员 = “ValidLayer” ‘ 必要に応じてレイヤー操作
Else
m_InternalShape.CellsU(PROP_STATUS).FormulaU = “1” ‘ 1: 異常
‘ 視覚的フィードバック:赤枠に変更
m_InternalShape.CellsU(“LineColor”).FormulaU = “RGB(255,0,0)”
End If
Validate = isValid
End Function
‘ — プライベートヘルパーメソッド —
Private Sub EnsurePropertyExists(ByVal cellName As String, ByVal prompt As String)
‘ ShapeSheetにカスタムプロパティ(Prop.xxx)が存在しない場合、動的に追加する
On Error Resume Next
Dim testVal As Double
testVal = m_InternalShape.CellsU(cellName).ResultIU
If Err.Number <> 0 Then
‘ プロパティが存在しないため追加
m_InternalShape.AddCustomProperty cellName, prompt, visCustomPropString, “”
End If
On Error GoTo 0
End Sub
—
3. コレクションクラスによる一括管理
単体のラップクラスを作成したら、次はページ全体の図形を走査し、このカスタムオブジェクトのコレクション(Collection)として管理するファクトリ層を構築する。
標準モジュール / コレクション処理:`DeviceManager.bas`
Option Explicit
Public Sub ProcessActivePageDevices()
Dim vsoPage As Visio.Page
Set vsoPage = ActivePage
Dim devList As Collection
Set devList = New Collection
Dim vsoShape As Visio.Shape
Dim wrappedDev As DeviceShape
‘ パフォーマンス最適化:画面描画とイベントを一時停止
Application.ScreenUpdating = False
Application.EventsEnabled = False
On Error GoTo ErrorHandler
Dim shp As Visio.Shape
For Each shp In vsoPage.Shapes
‘ 特定のマスター図形、またはカスタムプロパティを持つ図形のみを対象とする
If IsTargetShape(shp) Then
Set wrappedDev = New DeviceShape
wrappedDev.Initialize shp
‘ 業務データの読み書き・検証を実行
Debug.Print “Processing: ” & wrappedDev.Name & ” [Model: ” & wrappedDev.ModelNumber & “]”
‘ 価格の設定例
If wrappedDev.UnitPrice = 0 Then
wrappedDev.UnitPrice = 150000
End If
‘ 検証実行
If Not wrappedDev.Validate() Then
MsgBox “図形 ID: ” & wrappedDev.ID & ” のデータに不備があります。”, vbExclamation
End If
devList.Add wrappedDev
End If
Next shp
CleanUp:
‘ 復元
Application.ScreenUpdating = True
Application.EventsEnabled = True
Exit Sub
ErrorHandler:
MsgBox “予期せぬエラーが発生しました: ” & Err.Description, vbCritical
Resume CleanUp
End Sub
Private Function IsTargetShape(ByVal shp As Visio.Shape) As Boolean
‘ 例:特定のシェイプ名やマスター名でフィルタリング
‘ マスター名に “Server” が含まれる場合を対象とする
On Error Resume Next
If Not shp.Master Is Nothing Then
If InStr(1, shp.Master.Name, “Server”, vbTextCompare) > 0 Then
IsTargetShape = True
Exit Function
End If
End If
On Error GoTo 0
IsTargetShape = False
End Function
—
4. チーフアーキテクトが教える極限の知見:メモリ最適化とレガシー環境の罠
大規模なVisio図面(数千個のシェイプを持つプラント図やネットワーク図)をVBAで処理する際、以下の罠に直面する。
1. 循環参照によるメモリリークの回避
VBAのガベージコレクタは参照カウント方式(Reference Counting)を採用している。クラス内部で `Visio.Shape` を保持し、さらに `Shape` 側から何らかの形でVBAオブジェクトを参照させたり(あるいは不適切なグローバル変数への保持)、循環参照を作ると、インスタンスがメモリから解放されず、VBAのメモリリーク(Heap Corruption)を引き起こす。
- 鉄則: クラスのライフサイクルが終わるとき(`Class_Terminate`)、保持しているすべてのCOMオブジェクト(`Visio.Shape`, `Visio.Page` 等)に対して `Set xxx = Nothing` を明示的に実行し、参照を切断せよ。
2. COM相互運用におけるエラーハンドリングの徹底
Visio VBAでは、存在しないShapeSheetのセルにアクセスした瞬間に実行時エラー(エラー番号:-2147352567 / Automation Error)が発生する。これを防ぐために `On Error Resume Next` を多用しがちだが、スコープを適切に管理しないとバグの温床となる。
- 鉄則: プロパティのGetter/Setter内では厳格にエラーをトラップし、デフォルト値を返すか、例外を安全にラップして上位へ伝播させよ。
3. パフォーマンスの極限追求
`For Each shp In vsoPage.Shapes` による走査は、図形が多いほどCOM境界を跨ぐオーバーヘッドで激重になる。
- 鉄則: 処理の開始前には必ず `Application.ScreenUpdating = False` および `Application.EventsEnabled = False` をかけ、描画更新とイベントハンドリングのコストを完全に排除すること。これを怠ると、数千シェイプの処理に数分を要するが、適切に行えば数秒で完了する。
—
5. まとめ
VBAは「レガシーな簡易言語」と揶揄されることが多い。しかし、オブジェクト指向の原則(カプセル化、責務の分離)を適用すれば、耐障害性が高く、拡張性に優れたエンタープライズレベルのツールへと昇華させることが可能だ。
生(生体)の `Visio.Shape` を直接触るコードを書くのは今日で終わりにしよう。クラスモジュールによるラッパーレイヤーを一枚挟むこと。それこそが、破綻しないVisio自動化システム構築の唯一にして最大の王道である。
