【テクニカル・上級編】【中級】Shape.LayerMembershipによるレイヤー管理:特定のカテゴリの図形だけを非表示にする一括制御 – Visio VBA解析バイブル

スポンサーリンク

【Visio VBA極限攻略】Shape.LayerMembershipによる超高速レイヤー制御――COMバリアを突破するアーキテクチャ設計

Visioを用いた大規模な図面管理(プラント設計図、ネットワークトポロジ、大規模組織図など)において、特定のカテゴリに属する図形群を一括で非表示にしたり、編集ロックをかけたりする機能は、社内システム構築における必須要件です。

しかし、ナイーブに実装されたVisio VBAツールは、数千個のシェイプを処理する過程で容易にフリーズを引き起こします。その原因のほとんどは、COM(Component Object Model)の境界をまたぐ不要な往復(Round-trip)と、Visioの再計算エンジンの暴走にあります。

本稿では、シェイプのレイヤー所属情報を劇的に高速で判定する`Shape.LayerMembership`プロパティに焦点を当て、ミリ秒単位で数万シェイプを制御する極限の最適化アーキテクチャを解説します。

—

1. レイヤー制御におけるボトルネックの正体

多くの開発者が陥る典型的なアンチパターンは、以下のような構造です。

‘ 【アンチパターン】COMバリアを毎回越える極めて遅いループ
For Each shp In pg.Shapes
For i = 1 To shp.LayerCount
If shp.Layer(i).Name = “Non-Display” Then
‘ 処理
End If
Next i
Next shp

このコードが致命的に遅い理由は2つあります。

1. COMバリアの横断回数(Chatty API):
`shp.Layer(i)` を呼び出すたびに、VBA実行エンジンとVisio本体の間でCOMプロセス間通信(またはスレッド間通信)が発生します。さらに、`Name`プロパティを取得するために再度COM通信が発生します。シェイプ数が5,000、それぞれが平均2つのレイヤーに属している場合、数万回ものCOMバリア横断が発生し、数秒〜数十秒のフリーズを招きます。
2. 再描画と再計算のトリガー:
プロパティを1つ変更するたびに、Visioはドキュメント全体の再計算(依存関係の解決)と画面描画を試みます。

救世主としての `Shape.LayerMembership`

このボトルネックを突破するために、Visioオブジェクトモデルには `Shape.LayerMembership` という隠れた超強力なプロパティが用意されています。

  • `Shape.LayerMembership` プロパティ:

そのシェイプが所属しているレイヤーのインデックス(1ベース)を、カンマ区切りの文字列(例: `”1,3,5″`)で一括返却します。どのレイヤーにも属していない場合は、空文字列(`””`)を返します。

このプロパティを「レイヤー名とインデックスの事前マッピング(ハッシュテーブル化)」と組み合わせることで、COM通信を最小限に抑え、O(1)の計算量で特定のレイヤーに所属しているかを判定可能になります。

—

2. 極限高速化レイヤー制御エンジンの設計思想

我々が構築するプロダクションコードの設計思想は以下の通りです。

1. 画面更新・イベント・再計算の一時停止(三大最適化):
`ScreenUpdating`、`EventsEnabled`、`DeferRecalc` を確実に制御します。
2. 高速プロファイリング:
Windows APIの `QueryPerformanceCounter` を用いて、処理時間をマイクロ秒精度で計測し、最適化の効果を可視化します。
3. 事前キャッシュ戦略:
対象ページの全レイヤー名とインデックスの対応関係を、`Scripting.Dictionary`を用いてメモリ上にキャッシュします。
4. 確実なエラーハンドリングとリソース解放:
途中でエラーが発生した場合でも、必ずVisioの描画・再計算設定を元の状態に復元する堅牢な構造(Structured Exception Handlingの模倣)を構築します。

—

3. 【実戦コード】高速レイヤー制御エンジン(VBA完全版)

以下のコードは、指定したレイヤーに所属するシェイプのみを一括非表示(または表示)、あるいはロック/アンロックするための、実戦仕様のVBAモジュールです。標準モジュールに貼り付けて使用してください。

Option Explicit

‘ — Win32 API for High-Resolution Performance Counter —
If VBA7 Then
Private Declare PtrSafe Function QueryPerformanceCounter Lib “kernel32” (lpPerformanceCount As Currency) As Long
Private Declare PtrSafe Function QueryPerformanceFrequency Lib “kernel32” (lpFrequency As Currency) As Long
Else
Private Declare Function QueryPerformanceCounter Lib “kernel32” (lpPerformanceCount As Currency) As Long
Private Declare Function QueryPerformanceFrequency Lib “kernel32″ (lpFrequency As Currency) As Long
End If

”’

”’ 特定のレイヤーに属するシェイプの表示/非表示、およびロック状態を高速に一括制御する
”’

Public Sub BulkControlLayerVisibility( _
ByVal targetPage As Visio.Page, _
ByVal targetLayerName As String, _
ByVal makeVisible As Boolean, _
ByVal makeLocked As Boolean)

Dim startTime As Currency, endTime As Currency, freq As Currency
QueryPerformanceFrequency freq
QueryPerformanceCounter startTime

‘ Visioアプリケーション状態の退避用変数
Dim appState_ScreenUpdating As Boolean
Dim appState_EventsEnabled As Boolean
Dim appState_DeferRecalc As Boolean

Dim vsoApp As Visio.Application
Set vsoApp = targetPage.Application

‘ 1. アプリケーション状態の最適化退避
On Error GoTo ErrorHandler
appState_ScreenUpdating = vsoApp.ScreenUpdating
appState_EventsEnabled = vsoApp.EventsEnabled
appState_DeferRecalc = vsoApp.DeferRecalc

vsoApp.ScreenUpdating = False
vsoApp.EventsEnabled = False
vsoApp.DeferRecalc = True ‘ 再計算を保留

‘ 2. レイヤー名からインデックスへのマップを事前構築 (1-based index)
Dim layerMap As Object
Set layerMap = CreateObject(“Scripting.Dictionary”)

Dim vsoLayers As Visio.Layers
Set vsoLayers = targetPage.Layers

Dim i As Long
Dim vsoLayer As Visio.Layer
For i = 1 To vsoLayers.Count
Set vsoLayer = vsoLayers.Item(i)
‘ 辞書に「レイヤー名」をキー、「インデックス文字列」を値として格納
‘ LayerMembershipはカンマ区切り文字列を返すため、比較を容易にするため前後にカンマを付与
layerMap.Add LCase(vsoLayer.Name), “,” & CStr(i) & “,”
Set vsoLayer = Nothing
Next i

‘ 対象レイヤーのインデックス文字列を特定
Dim searchToken As String
Dim normalizedLayerName As String
normalizedLayerName = LCase(targetLayerName)

If Not layerMap.Exists(normalizedLayerName) Then
Err.Raise vbObjectError + 513, “BulkControlLayerVisibility”, _
“指定されたレイヤー ‘” & targetLayerName & “‘ はページ内に存在しません。”
End If
searchToken = layerMap.Item(normalizedLayerName)

‘ 3. レイヤー自体の属性変更(表示・ロック)
‘ ※Visioでは、レイヤー自体の表示属性を切り替えるアプローチが最速ですが、
‘ 個々のシェイプの LayerMembership を用いた判定は、個別シェイプの特定プロパティ変更に必須です。
‘ ここでは「特定のレイヤーに属するシェイプ群」に対し、個別処理を行うための高速走査を実装します。
Dim targetLayer As Visio.Layer
Set targetLayer = vsoLayers.Item(targetLayerName)

‘ セル(Row)を直接操作し、高速にレイヤー全体の表示/非表示・ロックを設定
targetLayer.CellsC(visLayerVisible).ResultIU = IIf(makeVisible, 1, 0)
targetLayer.CellsC(visLayerLock).ResultIU = IIf(makeLocked, 1, 0)

‘ 4. [応用] レイヤー所属シェイプに対する個別プロパティの超高速走査・処理
Dim vsoShapes As Visio.Shapes
Set vsoShapes = targetPage.Shapes

Dim vsoShp As Visio.Shape
Dim membership As String
Dim shpCount As Long
shpCount = vsoShapes.Count

‘ COMバリア横断を抑えるためのループ
For i = 1 To shpCount
Set vsoShp = vsoShapes.Item(i)
membership = vsoShp.LayerMembership

If Len(membership) > 0 Then
‘ 前後にカンマを付与し、部分一致による誤判定を防ぐ (例: “1” と “11” の混同防止)
membership = “,” & membership & “,”
If InStr(1, membership, searchToken, vbBinaryCompare) > 0 Then
‘ — ここに対象シェイプに対する個別処理を記述 —
‘ 例: ShapeTextの変更、ユーザー定義セルの書き換え、カスタムプロパティの同期など
‘ (今回は一括制御のデモとして、シェイプの文字にデバッグ情報を付与)
‘ vsoShp.Text = “[Targeted] ” & vsoShp.Name
‘ ———————————————–
End If
End If
Set vsoShp = Nothing ‘ ループ内で明示的にCOM参照を解放
Next i

CleanUp:
‘ 5. アプリケーション状態の確実な復旧
vsoApp.DeferRecalc = appState_DeferRecalc
vsoApp.ScreenUpdating = appState_ScreenUpdating
vsoApp.EventsEnabled = appState_EventsEnabled

‘ オブジェクトの明示的解放
Set layerMap = Nothing
Set targetLayer = Nothing
Set vsoLayers = Nothing
Set vsoShapes = Nothing
Set vsoApp = Nothing

QueryPerformanceCounter endTime
Dim elapsedSec As Double
elapsedSec = CDbl(endTime – startTime) / CDbl(freq)

Debug.Print “— Layer Control Execution Completed —”
Debug.Print “Processing Time: ” & Format(elapsedSec, “0.0000”) & ” seconds.”
Exit Sub

ErrorHandler:
MsgBox “エラー発生: ” & Err.Description, vbCritical, “Error in BulkControlLayerVisibility”
Resume CleanUp
End Sub

—

4. アーキテクチャ解説:なぜこのコードは「極限」なのか

1. `InStr` による高速文字列マッチングの極意

`Shape.LayerMembership` は、例えば `”1,3,12″` のような文字列を返します。
ここで、単に `InStr(membership, “1”)` と検索してしまうと、レイヤー「`1`」をターゲットにしているにもかかわらず、レイヤー「`12`」や「`11`」に所属しているシェイプまで誤ってマッチしてしまいます。

このバグを完全に排除し、かつ文字列処理を高速に行うため、以下の「カンマ包囲アルゴリズム」を採用しています。

  • 探索対象(ターゲットのレイヤーインデックスが `1` の場合): `,1,`
  • 被探索文字列(`LayerMembership` が `”1,3,12″` の場合): 前後にカンマを付与して `,1,3,12,` に変換。
  • `InStr(“,1,3,12,”, “,1,”)` は `1` を返し、正しくマッチします。
  • 被探索文字列が `”3,12″`(カンマ付与後 `,3,12,`)の場合、`,1,` はマッチしません。

正規表現(RegExp)オブジェクトをインスタンス化してマッチングを行う方法もありますが、VBAにおいては `InStr`(バイナリ比較 `vbBinaryCompare`)が最も低レベルで最適化されており、最速です。

2. COMオブジェクトのライフサイクル管理

ループ処理内で `Set vsoShp = Nothing` を明示的に行っています。
VBAのガベージコレクションは参照カウンタ方式であり、スコープを抜けるまでメモリ解放が遅延することがあります。数万回のループを回す場合、一時的なCOMラッパーオブジェクトがメモリを圧迫し、L1/L2キャッシュの汚染や、最悪の場合はメモリ不足(Out of Memory)を引き起こします。ループの終端で明示的に `Nothing` を代入することは、レガシーかつシニアなVBA開発において極めて重要なプラクティスです。

—

5. VB.NET / VSTO(アドイン)移植への布石

このコードをVB.NETやC#によるVisio VSTOアドインへ移植する場合、COM参照リークの防止が最優先課題となります。.NETのガベージコレクタ(GC)は、COMオブジェクトの解放タイミングを感知できないため、明示的に `Marshal.ReleaseComObject` を実行しなければ、Visioのプロセス(`VISIO.EXE`)がバックグラウンドに残留し続けるゾンビプロセス問題を引き起こします。

VB.NETでのループ内COM解放の実装例

‘ VB.NET (VSTO) での安全なループ実装例
Dim vsoShapes As Visio.Shapes = targetPage.Shapes
Dim shpCount As Integer = vsoShapes.Count

For i As Integer = 1 To shpCount
Dim vsoShp As Visio.Shape = vsoShapes.Item(i)
Try
Dim membership As String = vsoShp.LayerMembership
If Not String.IsNullOrEmpty(membership) Then
membership = “,” & membership & “,”
If membership.Contains(searchToken) Then
‘ 個別処理
End If
End If
Finally
‘ COM参照を確実にデクリメント
If vsoShp IsNot Nothing Then
System.Runtime.InteropServices.Marshal.ReleaseComObject(vsoShp)
vsoShp = Nothing
End If
End Try
Next

‘ 親オブジェクトの解放も忘れずに行う
System.Runtime.InteropServices.Marshal.ReleaseComObject(vsoShapes)

.NET環境下では、`For Each` による反復処理は、暗黙的にEnumeratorのCOMオブジェクトを生成するため避けるべきです。上記のように、インデックスによる `For` ループを用い、確実に `ReleaseComObject` を呼ぶのがエンタープライズ開発における鉄則です。

—

6. まとめ

Visio VBAにおけるパフォーマンスチューニングは、いかに「Visio本体との通信回数を減らすか」と「再計算・再描画の発生を抑えるか」の2点に集約されます。

今回紹介した `Shape.LayerMembership` と、事前マッピング辞書を組み合わせた手法は、これら2つの要件を完璧に満たす究極のアプローチです。この設計パターンをマスターすれば、図面管理の自動化において、競合他社のツールを圧倒する極限の処理速度を実現できるでしょう。

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