Excelで図形・画像を扱う際の便利な機能として、「図形のグループ化」があります。
グループ化を実行すると、そのグループをひとつの図形かのように操作でき、
その上で内部図形をクリックしたときは単体の図形を操作できます。

この機能、とても便利なのですがVBAで図形を扱う場合は注意が必要で、
以下のコードは10個のTextBoxではなく、
5個のTextBoxと1個のグループを取得してしまいます。

単にWorksheetオブジェクトのShapesをForEachで取得するだけだと、
表にあるグループしか取得できないということですね。
よってこのシートにおいてTextBox6~10を取得する場合は、
Shapes("Group 11")のGroupItemsの中身を取得する必要があるのですが、
さらに厄介なのが「グループは階層化できる」という仕様です。
要するに「グループ内にもグループが存在する可能性がある」訳ですね。
これをすべて単独図形のShapeとして取得する場合は、
フォルダ内のフォルダを展開するような、再帰呼出が必要になります。
今回はこの再帰呼出を行うコードを解説します。
基本コード
' シート上の全図形への処理 Sub シート上にあるグループ内を含むすべての図形を処理する() Dim ws対象シート As Worksheet Set ws対象シート = ActiveSheet ' 対象シート内のすべてのShapeをループ Dim シェイプ As Shape For Each シェイプ In ws対象シート.Shapes ' Shapeがグループの場合 If シェイプ.Type = msoGroup Then ' グループ内のShapeへの処理 Call グループ内の全シェイプへ処理を実行し下層グループへ再帰する(シェイプ) ' Shapeが単体図形の場合 Else ' ★ここに各Shapeへの処理 Debug.Print シェイプ.Name End If Next End Sub ' グループ内のShapeへの処理 Private Sub グループ内の全シェイプへ処理を実行し下層グループへ再帰する(グループシェイプ As Shape) ' 対象グループ内のすべてのShapeをループ Dim シェイプ As Shape For Each シェイプ In グループシェイプ.GroupItems ' Shapeがグループの場合 If シェイプ.Type = msoGroup Then ' 再帰呼出 Call グループ内の全シェイプへ処理を実行し下層グループへ再帰する(シェイプ) ' Shapeが単体図形の場合 Else ' ★ ここに各Shapeへの処理 Debug.Print シェイプ.Name End If Next End Sub
解説
Worksheet内のShapesコレクションをループし、
- シェイプが単独の図形であればその図形に処理を実行
- シェイプがグループであれば、その中にあるシェイプにも処理を実行
- グループの中にあるグループに対して2を再帰呼出
という順を踏んでいるコードです。
グループであるかの判定はShapeオブジェクトのTypeプロパティ、
グループ内のループにはGroupItemsプロパティを使用しています。
これらの詳しい解説はこちらの記事をご覧ください。
あとは再帰呼出を使い、Groupでなくなる最深部までShapeを取得しています。
再帰処理を理解するのは慣れないと大変と思いますが、
再帰の仕組み自体はわからなくてもこのコードを使えば動きます。
興味がわいたら読み解いてみてください。
再帰処理におけるコード重複への対応
上記のコードで問題になるのが、↓これが二か所登場してしまっている点です。
' ★ここに各Shapeへの処理 Debug.Print シェイプ.Name
今回は親がシートの(グループでない)図形は再帰内で処理できないため、
どうしてもこのコードを1ヶ所にすることが出来ません。
簡単に対応する場合はここを関数化し、
Call 各シェイプへの処理(シェイプ)
としてしまうことで、コードの記載を1ヶ所に抑えることが出来ます。
通常はこの対応で問題ないと思いますが、
今回はもうひとつ、Shapeの取得を関数化する方法を紹介します。
そのコードがこちらになります。
' シェイプ取得関数の利用コードサンプル Sub シート上にあるグループ内を含むすべての図形を処理する() ' 汎用関数を用いてグループ内を含むすべての図形(Shape)をCollectionとして取得 Dim Shapeリスト As Collection Set Shapeリスト = Getシート上のグループ内を含む全ShapeのCollection(ActiveSheet) ' あとはそのコレクションに対してFor Eachループするだけ Dim シェイプ As Shape For Each シェイプ In Shapeリスト ' ★ ここに各Shapeへの処理 Debug.Print シェイプ.Name Next End Sub
' シート上の全Shapeの取得関数 Function Getシート上のグループ内を含む全ShapeのCollection(対象シート As Worksheet) As Collection Dim Clct結果値 As New Collection ' 対象シート内のすべてのShapeをループ Dim シェイプ As Shape For Each シェイプ In 対象シート.Shapes ' Shapeがグループならグループ内の全Shape取得して結果値に追加 If シェイプ.Type = msoGroup Then Dim Clct下層グループShapes Set Clct下層グループShapes = Getグループ内の全ShapeCollection(シェイプ) Dim 下層グループシェイプ As Shape For Each 下層グループシェイプ In Clct下層グループShapes Clct結果値.Add 下層グループシェイプ Next ' Shapeが単体図形ならそのまま結果値に追加 Else Clct結果値.Add シェイプ End If Next Set Getシート上のグループ内を含む全ShapeのCollection = Clct結果値 End Function ' グループ内の全Shapeの取得関数 Function Getグループ内の全ShapeCollection(グループシェイプ As Shape) As Collection Dim Clct結果値 As New Collection ' 対象グループ内のすべてのShapeをループ Dim シェイプ As Shape For Each シェイプ In グループシェイプ.GroupItems ' Shapeがグループならグループ内の全Shape取得して結果値に追加 If シェイプ.Type = msoGroup Then ' 再帰呼出 Dim Clct下層グループShapes Set Clct下層グループShapes = Getグループ内の全ShapeCollection(シェイプ) Dim 下層グループシェイプ As Shape For Each 下層グループシェイプ In Clct下層グループShapes Clct結果値.Add 下層グループシェイプ Next ' Shapeが単体図形ならそのまま結果値に追加 Else Clct結果値.Add シェイプ End If Next Set Getグループ内の全ShapeCollection = Clct結果値 End Function
解説
今回の処理を「対象Shapeの取得」⇒「各Shapeへの処理」に分け、
Shapeの取得を関数化しているコードです。
見てわかる通り、メインプロシージャを相当簡単に書くことが出来ており、
特にメインプロシージャ内に再帰がなくなるのが大きいメリットですね。
Shapeの取得関数の内容は、
- 再帰呼出を用いてすべてのShapeをループしていく
- 見つけたShapeをCollectionに格納していく
- 最終的に全ShapeのリストをCollectionとして返す
という仕様になっています。
このようにCollectionとして返す関数にしておくことで、
以降メインコードでは単純なFor Each文だけの処理で済むようになります。
再帰がメインコードに入ってこないだけでも相当読みやすくなりますし、
コードの再利用性も圧倒的にこちらが優れていますからね。
グループが関わるシェイプの処理にはこの関数を使っておけばいいわけです。
今回のようにループが複雑になるマクロを作る際は、
- 初回のループでは一旦取得するだけにしておく
- 取得したものは配列やCollectionに入れておく
- メインの処理はそのリストへの単独For Each文で書く
この手法をとれると劇的にメンテナンスしやすいコードになります。
「まずは取得に集中する」というテクニックを覚えておきましょう。
ちなみにこの手法は同じような再帰処理を行う、
「フォルダ内の全ファイルへの処理」でも利用することが出来ます。
こちらのパターンの汎用関数も作成していますので、
よろしければあわせてご利用ください。