2022/10/25

図形グリッド吸着のON-OFF制御




Excelには、ワークシート上の図形の移動時や変形時に「セルの枠線に合わせて配置」できる機能があります。
その方法としては、「Altキー」を押しながら図形をドラッグすることで実現できますし、また図1のように「図形を枠線に合わせる」機能(=グリッド吸着機能)をONにしておくことでも実現できます。
今回は、そのグリッド吸着機能のON-OFFを切り替えるマクロについて紹介します。

1.図形グリッド吸着のON-OFF設定(手動)

図形のグリッド吸着は図1のように、図形を選択状態①にしてからリボンの「図形の書式」タブ→「配置」グループ→「配置」→「枠線に合わせる」をクリック②する事で切り替えます。
「枠線に合わせる」文字列の前のマークが選択状態(グリッドON)がグリッド吸着ON状態、背景無し状態(グリッドOFF)がグリッド吸着OFF状態です。ONとOFFはトグルになっているのでクリックするたびに切り替わります。
グリッド吸着の設定
図1


グリッド吸着ON状態であれば、図2のように図形を移動・変形した時に「セルの枠線に合う」ように吸着してくれます。
グリッド吸着の状態
図2


2.マクロによる図形グリッド吸着のON-OFF

2-1.ボタンのオブジェクト

Excel上で表示されるボタンには、識別子(ID)が割り振られています。今回の「枠線に合わせる」というボタンは549番です。そのボタンオブジェクトは、以下の様に表すことができます。

 「Application.CommandBars.FindControl(ID:=549)

内容は「Excel(Application)のコマンドバー(CommandBars)の中から、IDが549番のコントロールを取り出した(FindControl)もの」という意味になります。
これ以外のボタンにも、当然ながら識別子が割り振られています。どのボタンが何番かは「コントロールID 一覧」などでサイト検索すると見つかりますが、下の「3.コマンドボタンのID値とidMso値の取得方法」でも取得方法を紹介しています。

この「枠線に合わせる」ボタンのオブジェクト(CommandBarButton)には多くのプロパティがありますが、今回は「ボタンの状態(ON か OFF か)」を取得する必要があるので「Stateプロパティ」を使用します。使い方は以下のようになります。

 「Application.CommandBars.FindControl(ID:=549).State

Stateプロパティで得られる値は図3です。
MsoButtonStates列挙型
定数内容
msoButtonDown-1ボタンが押されている
msoButtonMixed2
msoButtonUp0ボタンは押されていない
図3


図3では3種類の値となっていますが、今回の「枠線に合わせる(ID:=549)」ボタンでは、ボタンが押されている時は「-1」の値となります。もう一方の msoButtonMixed(値=2)は、別なボタンでの値のようです(但し、内容が「押されている」と msoButtonDown側と同じなので、どの様な状態を表しているのか分かりません)。

また、CommandBarButtonにはいくつかのメソッドがあります。今回は「ボタンをON-OFF」して「グリッド吸着有無の切り替え」を行う必要があるので「Executeメソッド」を使用します。使い方は以下のようになります。

 「Application.CommandBars.FindControl(ID:=549).Execute

なお、ボタンが押されている・押されていない はトグルで変わりますので、「押されている時(ON状態)にExecuteメソッドを実行すれば、押されていない状態(OFF状態)」になり、押されていない時(OFF状態)にExecuteメソッドを実行すれば、押されている状態(ON状態)」になります。
ですので、StateプロパティとExecuteメソッドを組み合わせて使うことにします。

2-2.呼び出し側

もし図形の変形もマクロ側から行うのであれば、マクロ内で位置計算をして枠線に合わせて変形させれば良いので、わざわざ「グリッド吸着」を使う意味はありません。ですので今回のマクロは、ユーザーが「図形を変形」させる時に、ユーザーが図形を「どの位置に変形させたかった」のかという「ユーザーの意思」をくみ上げ易くするために使うものだと考えています。

そのため図4のように、ユーザー操作を許可する前後にマクロを呼び出して「グリッド吸着ON-OFFを切り替え」ます。
なお、図1のようにグリッド吸着ON-OFFは手動でも切り替えられるため、ユーザーの設定状態を事前に保管しておき、操作終了後の吸着OFF命令時には「ユーザーが指定していた元の設定状態に戻す」事にします。ですので、吸着OFF命令をしても、ユーザーの元の設定がONであれば「ONのまま」ということもあり得ます。
  1. '========== ⇩(1) 呼び出し側 ============
  2. Sub Test()
  3.  Dim myGrid As Boolean    '←ユーザーの元のグリッド条件
  4.  Call GridChange(True, myGrid)    '←グリッド吸着をON
  5. '  ユーザーによる図形の操作
  6.  Call GridChange(False, myGrid)    '←グリッド吸着をOFF
  7. End Sub
図4


02行目「Dim myGrid As Boolean」は、ユーザーが設定したグリッド条件です。04行目で呼び出す「GridChange(図5のグリッド吸着をONするプロシージャ)」の第二引数として渡すことで、メインのプロシージャ(図4)内でユーザーのグリッド条件を記憶しておきます。
なお、myGridをモジュール変数で宣言するのもOKです。その場合は、グリッド吸着をON-OFFするプロシージ内の変数値変更がモジュールとして記憶されますので、GridChange(図5)の第二引数は不要となります。

06行目辺りでユーザーに図形の変形操作をやってもらうとして、その直前の04行目「Call GridChange(True, myGrid)」で、図5を呼び出します。第一引数にTrueを指定していますので、グリッド吸着をONに設定しています。
第一引数がTrueの場合、第二引数の「変数myGrid」の目的は「GridChange(図5) → メイン側(図4) にデータを流す」という役目になります。GridChangeプロシージャを第一引数Trueで実行すると、「ユーザーのグリッド設定条件」を取得してから、グリッド吸着をONに設定します。そしてGridChangeプロシージャが終了する時に「ユーザーグリッド条件」を変数myGridを介してメイン側(図4)に戻すという流れになります。
変数myGridはメインの中の変数ですので、ユーザーによる図形操作が終了する時点でも変数値は生きており、08行目でグリッド吸着をOFFする時に、メイン内で記憶していた「ユーザーのグリッド条件(変数myGrid)」を渡すことで、ユーザー条件を復元します。

06行目のユーザー図形操作の時点では、グリッド吸着がON状態になっていますので、ユーザーが図形を操作すると「セルの枠線に合う」ように変形・移動することになります。
ユーザー操作が完了したら、08行目「Call GridChange(False, myGrid)」で図5のGridChangeプロシージャを再び呼び出します。ユーザー操作終了後は「第一引数をFalse」としていますので、基本的にはグリッド吸着をOFF状態にします。但し、04行目で取得した「ユーザーのグリッド設定条件(=myGrid)」を第二引数で渡していますので、ユーザーがグリッドONを既定としていた場合は、ユーザーの既定(=ON状態)のままとします。

2-3.グリッド吸着切り替えプロシージャ

メイン(図4)の04行目・08行目から呼び出されるプロシージャが図5です。引数を2つ受け取ります。
第一引数「GridSetting_Get」は、グリッド吸着をON(=True)にするか、OFF(=False)にするかです。
第二引数「myGrid」は、ユーザーの既定のグリッド条件です。第一引数がTrueの場合は第二引数が「戻り値」となり、第一引数がFalseの場合は第二引数が「受け取り値」となります。
  1. '========== ⇩(2) グリッド吸着ON-OFF ============
  2. Public Sub GridChange(GridSetting_Get As Boolean, myGrid As Boolean)
  3.  Dim CurrentGridSet As Boolean     '←現状のグリッド吸着の状態
  4.  With Application.CommandBars.FindControl(ID:=549)
  5.   CurrentGridSet = .Control.State
  6.   Select Case GridSetting_Get
  7.    Case True
  8.     myGrid = CurrentGridSet
  9.     If myGrid = False Then
  10.      .Execute
  11.     End If
  12.    Case False
  13.     If myGrid = Not CurrentGridSet Then
  14.      .Execute
  15.     End If
  16.   End Select
  17.  End With
  18. End Sub
図5


24行目「With Application.CommandBars.FindControl(ID:=549)」では、以降のコードを「グリッド吸着ボタン(ID=549番)」を基準に実行していきます。
まず、26行目「CurrentGridSet = .Control.State」では、現状のグリッド状態を把握し、変数CurrentGridSetに代入します。Control.Stateで取得する値は「グリッド吸着ON=True」「グリッド吸着OFF=False」です。

28行目「Select Case GridSetting_Get」では、プロシージャの第一引数(True または False)の値によって分岐させます。
29行目「Case True」は、第一引数がTrue(グリッド吸着をONの指示)の時です。
まず30行目の「myGrid = CurrentGridSet」で、第二引数myGrid(メインへの戻り値)に現在のグリッド状態を保管します。
そして32行目「If myGrid = False Then」で、現状がFalse(グリッド吸着OFF)の時のみ、33行目の「.Execute」でグリッド吸着ONに切り替えます。現状が既にONであれば、切り替える必要が無いからであり、再びExecuteメソッドを実行してしまうと、吸着OFFになってしまうからです。

一方、36行目「Case False」は、プロシージャの第一引数がFalse(グリッド吸着をOFFの支持)の時です。
37行目「If myGrid = Not CurrentGridSet Then」では「現状がユーザー設定条件と異なる」時のみ、38行目「.Execute」でグリッド吸着ONに切り替えます。分かりにくいので、「保存されているユーザー設定」と「現状のグリッド設定」の関係を図6にまとめました。
ユーザー設定
(myGrid)
現状グリッド設定
(CurrentGridSet)
実行要否実行結果
TrueTrueTrue
False(図形操作中に変更).Execute
FalseTrue.ExecuteFalse
False(図形操作中に変更)
図6


ユーザーが図形操作時に「グリッド吸着ON-OFF操作」を行わなければ、単純にユーザー設定条件(myGrid)のみで分岐させれば良いのですが、操作は可能な状況です。ですので2つの条件を比較し、ユーザー設定(myGrid)と現状グリッド設定(CurrentGridSet)の値が「逆のもの」についてのみ38行目「.Execute」を実行しています。

3.コマンドボタンのID値とidMso値の取得方法

3-1.ボタンの表現方法

コマンドボタンは、Excelのリボンのボタンに紐付いています。そのボタンが、リボン上の「タブ」→「グループ」→「ボタン」と体系的に分かれて配置されているように、コマンドボタンも体系的になっています。今回の「枠線に合わせる」ボタンも549番というID番号が振られていますが、体系的には図7のように一番上のCommandBarコレクションを親とすると、549番のボタンは「玄孫(やしゃご)」の位置になります。
ボタンの体系的な位置
図7


図7の枝分かれ部分に記されている数字はIndexであり、またCommandBarsのカッコ内はNameになります。このIndexとNameを使って今回の「枠線に合わせる」ボタンを表現すると、図5の24行目で紹介した式(下記のNo.1)以外にも、色々な表し方が可能です。
  1. Application.CommandBars.FindControl(ID:=549)
  2. Application.CommandBars(84).Controls(1).Controls(5).Controls(1)
  3. Application.CommandBars("Drawing").Controls(1).Controls(5).Controls(1)
  4. Application.CommandBars("Draw").Controls(5).Controls(1)
  5. Application.CommandBars("Snap To").Controls(1)
またIDと似たような「idMso値」という文字列がボタンには割り振られており、今回の「枠線に合わせる」ボタンには「"SnapToGrid"」が設定されています。どのボタンに何というidMso値が設定されているかは、ボタンを配置する時にポップアップで表示されます。
手順としては、まずリボン上でマウス右クリックし「リボンのユーザー設定」を選択します。すると図8のようにExcelオプションのダイアログが開き、リボンのユーザー設定のページが表示されますので、必要なボタンの上にマウスを当てることでポップアップ上のカッコ内にidMso値が現れます。
ボタンのidMso値の調べ方
図8


このidMso値を使う場合、実行(=ボタンを押す)するにはExecuteメソッドでは無く下記のように「ExecuteMso」メソッドを使います。idMsoパラメータに、idMso値を設定して実行します。
  1. Application.CommandBars.ExecuteMso idMso:="SnapToGrid"
また、ボタンが押されているか否かを調べるには、Stateプロパティでは無く下記のように「GetPressedMso」メソッドを使います。戻り値としては図3のような数値では無く、「押されている=True、押されていない=False」となります。
  1. MsgBox Application.CommandBars.GetPressedMso(idMso:="SnapToGrid")
なお、このidMso値は「大文字小文字」を区別しているようです。正確に記述しないと「ボタンが見つからない状態」になるので注意が必要です。

寄り道
全てのボタンに、IDとidMsoの両方が必ず設定されているかを確認した訳ではありませんが、恐らく両方が設定されているのだと思います。その上で「ID」「idMso」のどちらを使えば良いかを試してみました。
適当にボタンを選んで確かめ始めたら、すぐに突き当りました。シート上の画像を変形させる「トリミング(画像を選択した状態で「図の形式」タブ→「サイズ」グループ→「トリミング」)」です。
調べてみると、IDは732番でidMso値は「PictureCrop」のようです。

idMso値を使ったExecuteMsoとGetPressedMsoは正しく動くのですが、IDを使ったExecuteとStateは動かないのです。732番が間違っているのかと思いましたが「トリミング」や「Trimming」は他には存在しません。

この一例だけで判断するのは性急だと思いますが、とりあえずidMso値が分かるのであれば、idMsoを使った方が良いかもしれません。ちなみに図5の24行目で使用したコードはIDを使っていますが、正しく動いています。

3-2.ボタンIDの一覧表作成

ここでは、Excel上のボタンのID一覧を作成するコードを紹介します。本当はボタンのIDと共にidMso値も一緒に一覧にできれば良いのですが、ボタンオブジェクトのプロパティ内にはidMso値が見つかりません(どこで情報を取得できるのか分かりません)。ですので以下のプログラムで取得できるのはボタンのID値のみで、idMso値については図8のように1つ1つ調べるしか、今のところ手段がなさそうです。

3-2-1.1層目の出力

コマンドバー+ボタンの体系とID値を取得するのが図9+図11です。コマンドバー+ボタンの体系は何層にもなっていますが「操作できるボタンが存在する層はバラバラ」です(少なくとも1層目にはボタンは存在しない)。
そのため図9側からプログラムを開始して「タイトル行」と「1層目」の書き出しを行った後は、図11を必要な回数だけ再帰呼び出ししながら「実ボタンが存在する層」を洗い出していきます。
  1. '========== ⇩(3) ボタン一覧(メイン) ============
  2. Sub make_CommandBarList()
  3.  Dim cb As Object    '←CommandBarオブジェクト
  4.  Dim Title As Variant   '←タイトル行の文字の配列
  5.  Dim i As Long     '←記入する行位置
  6.  Dim j As Integer    '←記入する列位置
  7.  Title = Array("Index", "ID", "Name", "NameLocal", "Index", "ID", "Name")
  8.  i = 1
  9.  Cells(i, 1).Resize(1, UBound(Title, 1) + 1) = Title
  10.  i = i + 1
  11.  j = 0
  12.  For Each cb In CommandBars
  13.   Cells(i, 1) = cb.Index
  14.   Cells(i, 2) = cb.ID
  15.   Cells(i, 3) = cb.Name
  16.   Cells(i, 4) = cb.NameLocal
  17.   Call search(cb, i, j)
  18.  Next cb
  19. End Sub
図9


57~59行目は一覧表のタイトル行の処理です。
57行目「Title = Array("Index", "ID", "Name", "NameLocal", "Index", "ID", "Name")」では、タイトル行の文字列を配列に代入しています。最初の4つ「"Index", "ID", "Name", "NameLocal"」が1層目のタイトルで、残り3つ「"Index", "ID", "Name"」が2層目のタイトルとなります。
とりあえず2層目までのタイトルとしましたが、実際にプログラムを流してみると5層目まではデータがあるので、全層にタイトルが欲しい方は項目追加して下さい。

58行目「i = 1」は、タイトル行とする行位置をカウンタ変数iに設定しています。
59行目「Cells(i, 1).Resize(1, UBound(Title, 1) + 1) = Title」では、58行目で設定した行位置に、貼り付けるサイズを調整してからタイトルの配列を貼り付けています。

61行目「i = i + 1」では、データを書き込む行位置をタイトル行から1つ下げています。
62行目「j = 0」の変数jは、図10のように「2層目以降の層の位置を表す番号」です。69行目で図11(searchプロシージャ)を呼び出して2層目以降を出力する際に使用します。
(変数jは初期値がゼロなので必須なコードではありませんが、あえて明示しています)
層と変数jの関係
図10


63行目「For Each cb In CommandBars」では、CommandBarsコレクションから1つずつCommandBarオブジェクトを取り出し、変数cbとしています。
その取り取り出したCommandBarオブジェクトの必要なプロパティを出力しているのが64~67行目です。今回は出力プロパティとして「Index」「ID」「Name」「NameLocal」を選び、順番に横方向に書き込んでいきます。
なお、この1層目の「ID」はCommandBarのID値ですので、例えば図10のB2セルに出力された「265番」を使って「Application.CommandBars.FindControl(ID:=265)」としても何も得られません(Nothingが戻ってくる)。と言って、どのように活用すれば良いのか、今のところ分かりません。

1層目の4列分の出力が終了したら、69行目「Call search(cb, i, j)」で図11を呼び出し、2層目以降の出力をしていきます。渡す引数としては以下の3つです。
 ・第一引数:cb = CommandBarオブジェクト
 ・第二引数:i = 現在の書き込み行位置
 ・第三引数:j = 層の位置(≒列方向の位置)
上記の内、第二引数「i」には、1層目と同じ i 値を渡していますので、2層目も1層目と同じ行から出力開始されます。
また第三引数「j」は、図9から図11を呼び出す際には j = 0 を渡しますので、図11内で2層目の出力をする時は、図10で言えばE~G列に書き込むことになります。しかし図11から図11を再帰呼び出しする際には3層目以降の列位置に出力する必要があるので、j値を1つずつ増やしながら再帰呼び出しをします。

3-2-2.2層目以降の出力

図9の69行目から呼び出されるのが図11です。また図11の呼び出し元は図9だけでは無く、図11の95行目からの場合もあります。自分から自分を呼び出しているので「再帰呼び出し」となります。
図11の受け取る3つの引数は「CommandBar等のオブジェクト(cb)」「親の層を出力した行位置(i)」「次に出力する層の位置(j)」です。
なお第一引数は、図9から呼び出される時は「CommandBarオブジェクト」を受け取り、再帰呼び出し時(=2層目以降)は「CommandBarPopupオブジェクト」を受け取っているようです。

図11の81行目「Sub search(cb As Object, ByRef i As Long, ByVal j As Integer)」のように、引数を受け取る際、第二引数の「行位置」は「参照渡し(byRef)」ですので、図11内で変数を変更させた時には呼び出し元にも変更が反映されることになります。つまり「行位置」を参照渡しにすることで、新たなデータを書き込む時には、シート上の次の行に出力されることになります。
(第一引数「CommandBarなどのオブジェクト」は何も指定していないので、既定の参照渡し(byRef)となります。)

一方、第三引数である「層の位置」は「値渡し(byVal)」ですので、呼び出し元には変更が反映されません。呼び出し元が図9の時には、図9内では「jの値を使って書き込みをしていない」ためにbyValである必要は無いのですが、図11の95行目から呼び出す(=再帰呼び出し)時には「下の層の出力が完了して制御が戻ってきたら、元の j 値を使って該当する層の位置に出力」しなければいけないために「値渡し(=呼び出し元の値を変更しない)」である必要があります。
  1. '========== ⇩(4) ボタン一覧(サブ) ============
  2. Sub search(cb As Object, ByRef i As Long, ByVal j As Integer)
  3.  Dim ctrl As Object    '←1つ1つのコントロール
  4.  For Each ctrl In cb.Controls
  5.   Cells(i, 5 + j * 3) = ctrl.Index
  6.   On Error Resume Next
  7.    If ctrl.CommandBar Is Nothing Then
  8.     Cells(i, 6 + j * 3) = ctrl.Control.ID
  9.     Cells(i, 7 + j * 3) = ctrl.Control.Caption
  10.     i = i + 1
  11.    Else
  12.     Cells(i, 6 + j * 3) = ctrl.CommandBar.ID
  13.     Cells(i, 7 + j * 3) = ctrl.CommandBar.Name
  14.     Call search(ctrl, i, j + 1)
  15.    End If
  16.   On Error GoTo 0
  17.  Next ctrl
  18. End Sub
図11


今回のコードは、各層のオブジェクトのプロパティの違いを使っています。オブジェクトにCommandBarプロパティが存在すれば「まだコマンドバーであり、その下にボタンが隠れている」ことになり、逆にCommandBarプロパティが存在しなければ「そのオブジェクトはボタン」となっているようなのです。
今回は、その「CommandBarプロパティ有無」で分岐させながら処理を行っていきます。

また2層目以降(=図11内)では、オブジェクトのプロパティを3種類出力させることにしました。但しオブジェクトが、ボタンであるか否かで出力内容が異なりますので、図12にまとめておきます。
ボタンコマンドバー
1オブジェクトのIndex番号
2ControlのID番号CommandBarのID番号
3ControlのCaptionCommandBarのName
図12


84行目「For Each ctrl In cb.Controls」で、引数として受け取ったオブジェクト(変数cb)を1つ1つのコントロールに分解します。
その分解した1つ1つのコントロールのIndex番号を85行目「Cells(i, 5 + j * 3) = ctrl.Index」で、シート上に出力します。1層目には4列を使用し、2層目以降は3列で1層ですので、列位置を表す式「5 + j * 3」は、5列目からの層の深さ(j * 3)の中の1番目の列としています。なお行位置は変数iです。

88行目「If ctrl.CommandBar Is Nothing Then」では、オブジェクトの中に「CommandBarプロパティが存在するか否か」を調べています。しかし実際には、CommandBarプロパティが存在しない時(オブジェクトがボタンの場合)には「ctrl.CommandBar」自体が存在しないためにエラーが発生し、88行目のIf文は評価されずに89~90行目のコードを実行します。逆にCommandBarプロパティが存在する場合(=まだボタンでは無い)には、88行目が評価されて92行目「Else」に飛び93~95行目を実行することになります。

89行目「Cells(i, 6 + j * 3) = ctrl.Control.ID」は、「3列で1層」の2番目の列に、ボタンのID値を書き込みます。
90行目「Cells(i, 7 + j * 3) = ctrl.Control.Caption」は、「3列で1層」の3番目の列に、ボタンのCaption値を書き込みます。
ボタンのプロパティ値の出力が完了したら、91行目「i = i + 1」で次の行に移動します。

オブジェクトがボタンでは無く、まだコマンドバーの場合は、93行目「Cells(i, 6 + j * 3) = ctrl.CommandBar.ID」で「3列で1層」の2番目の列にCommandBarのID値を書き込み、94行目「Cells(i, 7 + j * 3) = ctrl.CommandBar.Name」で「3列で1層」の3番目の列にCommandBarのName値を書き込みます。

そしてコマンドバーの場合は、更に分解するために95行目「Call search(ctrl, i, j + 1)」で、自分を再帰呼び出しします。
その際、分解した内容は「真横に移動して出力」させるように、第二引数「i」はそのままとし、また「右側の層に移動」させるために列位置である「変数j」を「j+1」として第三引数に指定します。引数に直接「増やしたj値」を指定することで、自分のプロシージャ内ではj値を不変にし、再帰呼び出し先から制御が戻ってきた後も、正しい層の位置に出力する事が出来ます。

なお、オブジェクト(変数ctrl)がボタンであった場合は、88行目の「ctrl.CommandBar」の部分でエラーが出るので、86行目「On Error Resume Next」でエラーをスルーさせています。

このプログラムで出力したものの一部が図13です。
出力の一部
図13


また、今回のテーマの「グリッド吸着ボタン」は、図14のように1層目が「CommandBars("Drawing")」の項にぶら下がっている形になります。このデータから、グリッド吸着のIDが549番と分かります。
このグリッド吸着の部分のみをスダレ風に表現したものが、図7になります。
グリッド吸着周辺のボタン類
図14


寄り道
上記プログラムをデータ型を確認しながら動かしてみると、図9内ではCommandBar型ですが、図11内で処理するオブジェクトは複数種類のデータ型で流れることが分かりました。そのため、図9の52行目・図11の81行目・82行目のオブジェクト変数のデータ型宣言を1つに絞ることが出来ず、「Object型」として指定せざるを得ませんでした。
本来は詳細なデータ型で指定をしたかったのですが、御了承下さい。

アプリ実例

マウス操作で日程の開始・完了を設定できるタスク表