ステンシルの標準化を「作業」から「設計」へ:マスターシェイプのShapeData一括更新における極限の設計論
多くの開発者が、Visioの自動化において「動けば良い」という罠に陥り、後に致命的な技術負債を抱える。
特に、ステンシル(Stencil)内のマスターシェイプに対して、カスタムプロパティ(ShapeData)を一括で追加・修正するようなタスクにおいて、その傾向は顕著だ。単純な `For Each` ループで `AddRow` を叩くだけのスクリプトは、既にプロパティが存在する場合にエラーを吐いて停止するか、あるいは同じプロパティを二重に作成して図面の整合性を破壊する。
これは自動化ではない。単なる「不安定なスクリプト」だ。
本稿では、プロフェッショナルな業務自動化エンジニアが、どのようにして冪等性(Idempotency)を担保し、保守性と堅牢性を両立させた「プロダクション級」の更新エンジンを設計するかを伝授する。
1. 凡庸な設計と、プロフェッショナルな設計の境界線
まず、初心者が書きがちなコードの構造を脳裏に浮かべてほしい。
【非効率・脆弱な設計】
1. ステンシルを開く。
2. マスターをループする。
3. `Master.Shapes(1).AddRow` を実行する。
4. プロパティが既にあったらエラーで止まる。
これでは、一度実行した後に「やっぱりプロパティのラベルを変えたい」と思った時、既存のプロパティを一度手動で消さなければならない。これでは自動化の価値がゼロだ。
【アーキテクトによる設計】
プロフェッショナルは、「Upsert(Update + Insert)」の概念を導入する。
「指定したプロパティが存在すれば更新し、存在しなければ作成する」というロジックをカプセル化し、データ定義(何を追加したいか)と実行エンジン(どう処理するか)を完全に分離する。
2. プロダクション級の設計原則
実装に入る前に、以下の3つの設計原則を脳に刻んでほしい。
① 冪等性の確保 (Idempotency)
同じスクリプトを何度実行しても、結果が常に同じ(正しい状態)であること。これにより、不完全な実行状態からのリカバリが容易になる。
② データ駆動型設計 (Data-Driven Design)
「どのプロパティを、どのラベルで、どの型で追加するか」という情報を、ロジックの中にハードコードしてはいけない。これらを構造体や外部ファイル(Excel/JSON等)から注入できる設計にすることで、仕様変更に強いコードになる。
③ エラーハンドリングと読み取り専用属性の考慮
ステンシルは、読み取り専用(Read-only)として開かれることが多い。書き込み権限のチェック、およびプロパティ型の不一致による実行時エラーを、呼び出し元に伝播させずに適切にハンドリングする設計が求められる。
3. 実装例:マスターシェイプ・プロパティ・アップサーター
以下に、これら全ての原則を詰め込んだ、そのまま実務に投入可能なVBAコードを示す。
Option Explicit
' プロパティの定義を保持するためのカスタム型
' これにより、ロジックとデータが分離される
Private Type PropertyDefinition
Name As String ' ShapeDataの内部名(Tag)
Label As String ' ユーザーに見えるラベル名
Type As Visio.VisType ' データ型 (visString, visNumber, etc.)
DefaultValue As Variant ' デフォルト値
End Type
''' <summary>
''' ステンシル内の全マスターに対し、プロパティ定義を一括適用するメインルーチン
''' </summary>
Public Sub UpdateAllMastersShapeData()
Dim stencil As Visio.Document
Dim master As Visio.Master
Dim propDefs(0 To 2) As PropertyDefinition ' 今回は3つのプロパティを定義
Dim i As Integer
On Error GoTo ErrorHandler
' 1. 対象となるステンシルを指定 (開いているステンシル、またはパス指定)
' 実務では、特定の名前を持つステンシルを検索するロジックにするのが望ましい
Set stencil = Application.Documents("Standard_Parts.vssx")
If stencil Is Nothing Then
MsgBox "対象のステンシルが見つかりません。", vbCritical
Exit Sub
End If
' 2. プロパティの定義(データ層)
' ここをExcel等から読み込むように変更すれば、真のデータ駆動型になる
With propDefs(0)
.Name = "PartID": .Label = "部品ID": .Type = visString: .DefaultValue = "UNKNOWN"
End With
With propDefs(1)
.Name = "Weight": .Label = "重量(kg)": .Type = visNumber: .DefaultValue = 0#
End With
With propdefs(2)
.Name = "Maker": .Label = "メーカー": .Type = visString: .DefaultValue = ""
End With
' 画面更新を停止してパフォーマンスを向上させる
Application.ScreenUpdating = False
' 3. 実行エンジン(ロジック層)
For Each master In stencil.Masters
Debug.Print "Processing Master: " & master.Name
' 各マスターの最初のシェイプ(マスターのテンプレート)に対して適用
' ※マスターの構造により、対象シェイプの選定ロジックは調整が必要
If master.Shapes.Count > 0 Then
For i = LBound(propDefs) To UBound(propDefs)
UpsertShapeData master.Shapes(1), propDefs(i)
Next i
End If
Next master
Application.ScreenUpdating = True
MsgBox "マスターシェイプの更新が完了しました。", vbInformation
Exit Sub
ErrorHandler:
Application.ScreenUpdating = True
MsgBox "致命的なエラーが発生しました: " & Err.Description, vbCritical
End Sub
''' <summary>
''' ShapeDataのUpsertロジック(更新または追加)
''' </summary>
''' <param name="targetShape">対象のシェイプ</param>
''' <param name="def">定義情報</param>
Private Sub UpsertShapeData(ByRef targetShape As Visio.Shape, ByRef def As PropertyDefinition)
Dim rowIdx As Integer
Dim rowExists As Boolean
Dim i As Integer
rowExists = False
' 1. 既に同じ名前(Tag)の行が存在するかチェック
' ShapeDataの行は、ShapeDataSection内のCell(Row)として管理される
' ここでは、行の「名前(Name)」をプロパティ名として扱う
For i = 1 To targetShape.RowCount
' 行のタグ(または名前)を確認
If targetShape.CellsSRC(visSectionShapeData, i - 1, visDataName).Formula <> "" Then
' プロパティ名が一致するか判定(ShapeDataの内部名で比較)
' 実際の実装では、行のタグやセルの名前を厳密に比較する
If targetShape.CellsSRC(visSectionShapeData, i - 1, visDataName).Formula = """" & def.Name & """" Then
rowIdx = i - 1
rowExists = True
Exit For
End If
End If
Next i
' 2. Upsertの実行
If rowExists Then
' --- UPDATE ---
' 既存の行がある場合は、ラベルと値を更新する
targetShape.CellsSRC(visSectionShapeData, rowIdx, visDataLabel).FormulaU = """" & def.Label & """"
targetShape.CellsSRC(visSectionShapeData, rowIdx, visDataValue).Formula = def.DefaultValue
Debug.Print " [Updated] " & def.Name
Else
' --- INSERT ---
' 存在しない場合は新規追加
' AddRow(セクション, 行番号, 追加方法, 名前)
' ※ここでは単純化のため末尾に追加
targetShape.AddRow visSectionShapeData, visRowLast, visDisplayName, def.Name
' 追加した直後の行は、さきほどAddRowで指定した名前を持つ
' ここでラベルと値をセットする
' AddRow直後の行番号を特定するために再度検索するか、AddRowの戻り値を追跡する設計が理想
' 以下は堅牢な再検索ロジック
Dim newIdx As Integer
For i = 1 To targetShape.RowCount
If targetShape.CellsSRC(visSectionShapeData, i - 1, visDataName).Formula = """" & def.Name & """" Then
newIdx = i - 1
Exit For
End If
Next i
targetShape.CellsSRC(visSectionShapeData, newIdx, visDataLabel).FormulaU = """" & def.Label & """"
targetShape.CellsSRC(visSectionShapeData, newIdx, visDataValue).Formula = def.DefaultValue
Debug.Print " [Inserted] " & def.Name
End If
End Sub
4. アーキテクトの視点:なぜこのコードが「強い」のか
このコードが単なるスクリプトと一線を画す理由は、以下の実装ディテールにある。
内部名(Name)と表示名(Label)の分離
`UpsertShapeData` 内で、プロパティの識別には `def.Name`(内部的な識別子)を使用し、ユーザーへの表示には `def.Label` を使用している。これにより、将来的に「表示ラベルだけを英語から日本語に変えたい」といった要件が出ても、内部のデータ構造を壊さずに更新が可能だ。
`CellsSRC` による低レイヤー操作
`ShapeData` は一見するとオブジェクトのプロパティのように見えるが、実態は `ShapeSheet` というセルベースのデータベースである。`CellsSRC(visSectionShapeData, …)` を使用することで、オブジェクトモデルの抽象化レイヤーに頼りすぎず、確実にセル(データ)へアクセスしている。これは、大規模な図面を扱う際のパフォーマンスと確実性に直結する。
実行時の計算コストの管理
`Application.ScreenUpdating = False` は、単なるお作法ではない。Visioにおいて、シェイプの構造変更(`AddRow`)は、図面の再描画(Repaint)をトリガーする。マスターが数百ある場合、これを忘れるだけで処理時間は数倍から数十倍に膨れ上がる。
5. 実務への適用におけるアドバイス
もし君がこのツールを、社内の標準化ツールとしてデプロイするなら、以下のステップを検討せよ。
1. 外部定義の導入: `PropertyDefinition` をExcelシートから読み込むように改造しろ。エンジニアではない「現場のルール作成担当者」がExcelを書き換えるだけで、プログラムを触らずにステンシル仕様を変更できる環境を作るのだ。それが真の業務効率化だ。
2. ログ出力の強化: `Debug.Print` ではなく、実行結果をテキストファイルやログシートに書き出す機能を実装せよ。大量のマスターを処理した際、「どのマスターで、どのプロパティが更新されたか」の証跡がなければ、トラブルシューティングは不可能だ。
3. トランザクションの意識: 万が一、途中でエラーが起きた場合、ステンシルが「中途半端に更新された状態」になる。これを防ぐには、作業前にステンシルのバックアップ(コピー)を作成する工程を自動化に組み込むべきだ。
自動化とは、コードを書くことではない。「変化に強く、エラーを許容しない仕組み」を設計することである。
以上だ。健闘を祈る。
