2020/03/04

セル色変更の情報をイベント風に受け取る




1.背景と概要

セルの値を変更した時は「Worksheet_Change」イベント等で変更情報を受け取れますが、セルの色(Interior.Color)や文字色(Font.Color)を変更してもイベントは発生しないことは良く知られています。しかし、この件に関しては他サイトのExcel 質問欄で良く見掛けるので、たぶんそういう要望・場面は多くあるのだろうと推測しています。
そのイベント代用策としては、「Worksheet_SelectionChange」イベントを使って「選択位置が変更されたら(何かセル操作をしたのだろうから)セル色の変更点を見つけに行く」というやり方を勧めているのが多い様です。しかし選択位置を動かさずにセル色を変更することは可能ですので「リアルタイムのイベント」とは言いきれません。

そこで「リアルタイムのイベント」に近いものを今回考えてみました。
一番下に添付したファイルの外観は図1-1の通りです。


図1-1

セルのB2~D10を太線で囲っていますが、この中を「セル色の変化を監視する計測範囲(=イベント監視範囲)」としています。また、「ON」と書かれたボタンは計測をスタート・ストップさせるボタンにしました。尚、ブックを開いた時には自動的にON(イベント取得状態)にしています。
(一番下のサンプルファイルではWorkbook_Open からのプロシージャ呼び出しは、コメントアウトしてあります。いきなりマクロが動くのもイヤなので。)

本システムの内容としては、一定時間ごとに計測範囲のセル色を見に行き、前回のセル色と違っていたらコメントを出す、というものです。一定時間を短くすると、あたかもリアルタイムのイベントの様な振る舞いになります。
極端な方法として「Do~Loop」で作ってみたところ一応単独では正常動作するのですが、「裏でマクロが常に動き続けている」という仕様はとても人に勧められるものではないので、OnTime式に落ち着きました。

2.シート上の細工

シート上のボタンには、図2-1の通り、マクロが割り当ててあります。標準モジュール(Module 1)の「Color_Start」プロシージャにつながっています。

図2-1

3.ブックモジュールのコード

図3-1のコードはブックモジュールに記載し、ブックを開いた時(Workbook_Open)に、自動的にマクロを動かすようにしています。
  1. '========== ⇩① ブックモジュール(ThisWorkbook) =======================
  2. Private Sub Workbook_Open()
  3.  Call Color_Start     '←添付ファイルではコメントアウトしてあります。
  4. End Sub
図3-1

セル色イベントマクロは、標準モジュール(Module1)の「Color_Start」プロシージャをスタートに動くように作りましたので、ブックを開いた時に「Color_Start」を動かしています。
Callステートメントは、他のプロシージャを呼び出して制御を渡す役目をしています。Call無しでプロシージャ名だけを書いても動作しますが、「他のプロシージャを呼び出している」ことが明示的になりますので、明記した方が良いと思います。

4.標準モジュールのコード

標準モジュール(Module 1)にコードを記載していきますが、流れが分かりにくいので、全体の流れ(図4-1)を先に説明しておきます。

図4-1

スタートプロシージャの「Color_Start」は、最初は Workbook_Open から呼び出されます。「Color_Start」プロシージャの中で マクロ開始・停止のフラグとしているColor_Event変数が反転(既定値False → True )され、その結果がTrueの場合は、次の「Cell_Color」プロシージャでセル色の初期値を取得し配列に格納します。
次に「Timer」プロシージャに入り、現時点でのセル色と「初期値との比較」し、もし異なっているセルがあればメッセージを出し、同時に初期値を更新します。
そのあとOnTimeメソッドで指定時間(サンプルマクロでは1秒)後に、自分を呼び出します(再帰呼び出しと言います)。呼び出された「Timer」プロシージャの中で再び「初期値との比較」をするのですが、この「初期値」は1つ前の「Timer」の中で更新されたデータとなります。
つまり、イベントの遅れ時間=1秒 ということになります。

次に、マクロが動いているときにボタンを押した事を考えてみます。マクロが動いている最中ですからフラグ変数「Color_Event」はTrueです。そのTrueの最中に「Color_Start」を動かしますので、Color_Event変数が反転( True → False )します。
Color_Event変数がFalseだと「Color_Start」プロシージャを途中で抜け出して停止するのですが、既にOnTimeで実施予約してしまった「Timer」プロシージャは止まりません。ですので、「Timer」プロシージャの先頭でColor_Event変数がFalseの時には抜け出して停止するようにしてます。

再びボタンを押して再開する際は、ブックを開いてWorkbook_Openイベントで「Color_Start」プロシージャを動かすのと全く同じです。

4-1.変数・定数の宣言

  1. '========== ⇩① 変数宣言部 =======
  2. Option Explicit          '←変数宣言の強要
  3. Private R As Range         '←セル色の計測範囲
  4. Private RA() As Long        '←色の番号を保存しておく配列
  5. Private Color_Event As Boolean  '←計測開始停止のフラグ
図4-2

4行目の変数「R」は、セル色の変化を計測する範囲を示す変数です。定数的な使用ですが、RangeオブジェクトはConstを使った定数設定が出来ません。ですので宣言部で変数宣言をし、図4-2の13行目で値代入をしています。
5行目の「RA()」は、計測範囲Rの色番号を保存しておく動的配列です。配列のサイズは計測範囲と同じで固定ですので静的配列として宣言することも可能ですが、一意的に決めるために、計測範囲をRに代入した後に計算(図4-3の15行目)で決めています。
6行目の「Color_Event」は、計測開始停止のフラグの役目をする変数です。変数の型をBooleanにしたのは、値を True ⇔ False と入れ替えることで、On-Offスイッチをトグルの様に作ることが出来る為です。
また今回はOnTimeを使って一定時間後にマクロが動くようにしてあるため、ボタンを押してストップさせても予定してしまったマクロは動いてしまいます。その為、動こうとしているマクロをこの「Color_Event」の値を使って停止させる役目を持たせています。


4-2.スタートプロシージャ

図4-3は「セル色変更イベントマクロ」のスタートプロシージャなっています。
  1. '========== ⇩② セル色イベントマクロのスタートプロシージャ ================
  2. Public Sub Color_Start()        '←WorkBook_Open、On-Offボタンで起動
  3.  Color_Event = Not Color_Event    '←計測開始停止フラグのトグル
  4.  Call Button1_Cap          '←ボタン表示の変更
  5.  If Color_Event = False Then Exit Sub   '←「停止」時はマクロSTOP
  6.  Set R = Sheets("sheet1").Range("b2:d10")  '←計測範囲値の代入
  7.                         '↓色番号保存の配列のサイズ指定
  8.  ReDim RA(R(1).Row To R(R.Count).Row, R(1).Column To R(R.Count).Column)
  9.  Call Cell_Color      '←現状把握用のセル色情報取得
  10.  Call Timer         '←タイマー稼働開始
  11. End Sub
図4-3

この「Color_Start」は、ブックモジュールのWorkbook_Openから起動され、またシート上のボタンからも起動されます。異なるモジュールからの呼び出しになりますので、スコープ(有効な範囲)を広げたPublicプロシージャにしています。逆にその他のプロシージャは、標準モジュール(Module 1)内でのやり取りなのでPrivateとしています。

9行目の「Color_Event = Not Color_Event」について説明します。変数Color_Eventは図4-2の6行目で宣言したままの状態ですから、Excelを立ち上げたばかりの状態では値はFalse(既定値)です。9行目の「=の右辺」は、その変数Color_Event(=False)に対してNotで反転させていますので、False → True になります。それをまた変数Color_Eventに代入していますので、Color_EventはTrueに変わるのです。
逆に現在Color_EventがTrueの状態であるならば、9行目の式でFalseに変わることになります。つまりボタンを押すたびに値が入れ替わる「トグルスイッチ」となるのです。

10行目は「Button1_Cap」プロシージャを呼び出しています。これは図4-5で説明しますが、シート上のボタンの表示を入れ替える( ON ⇔ OFF )ものです。

11行目は、変数「Color_Event」がFalseの時(=計測停止させた時)にプロシージャを抜け出してマクロを止めるものです。
変数「Color_Event」がFalseになるのは、シート上のボタンを押し、ボタンに登録されたマクロ「Color_Start」が動き、図4-3の9行目のコードを実施させた時になります。

13行目は、計測する範囲をモジュール変数R(Range のつもり)に代入しています。そして15行目で、セル色の番号を保存する配列RA(Range Array のつもり)の大きさを設定しています。
15行目は、動的配列として宣言したRAに対して、ReDimでサイズを決めています。配列RAのカッコ内「RA(R(1).Row To R(R.Count).Row, R(1).Column To R(R.Count).Column)」はちょっと複雑です。
セル範囲の大きさを決めているのですから、通常であれば「ReDim RA( 1 to 5,1 to 10 )」みたいな感じになるはずですので、そういう目で見てみます。
するとカッコ内の前半(カンマの前まで)は、「R(1).Row」 To 「R(R.Count).Row」です。まず「R(1).Row」の中で、セル範囲Rの後ろのカッコ内の「1」ですが、「範囲Rの1番目」という意味です。正確に書くと「R.Item(1).Row」になりますが、1番目とはどこでしょうか。
セルの位置を表す順番(.Item )は、図4-4の様に左上から右下に向かって数えていきます。ですので、右下のセルは範囲が含まれるセルの数(=R.Count)になります。

図4-4

今回の範囲Rは Range("b2:d10") ですので、 R(1) = Range("b2")、R(R.Count) = Range("d10"") を指します。ということは R(1).Row = 2 、R(R.Count).Row = 10 となり、カッコ内の前半は「 2 to 10 」と分かります。
カッコ内の後半「R(1).Column To R(R.Count).Column」も同じ考え方で「2 to 4」となります。
よって15行目は「ReDim RA(2 to 10,2 to 4)」で、範囲Rと行・列の番号を合わせた形でセル色番号を格納する配列が完成できました。

16行目は「Cell_Color」プロシージャを動かして、15行目でサイズを決定した配列に範囲Rのセル色番号を代入する工程です。
17行目は「Timer」プロシージャで、再帰呼び出しをしながらセル色の変化を監視続ける工程になります。

4-3.ボタンの文字変更

「Color_Start」プロシージャ内で「Color_Event」変数を反転(図4-3の9行目)した後で、シート上のボタンの表示を変更する必要があります。図4-3の10行目から呼び出されるのが、下図(図4-5)です。
  1. '========== ⇩③ ボタンの文字変更 ===================
  2. Private Sub Button1_Cap()
  3.  If Color_Event = True Then        '←フラグがTrue(計測開始)であればON
  4.   Sheets("sheet1").Shapes("Button 1").DrawingObject.Caption = "ON"
  5.  Else                 '←フラグがFalse(計測停止)であればOFF
  6.   Sheets("sheet1").Shapes("Button 1").DrawingObject.Caption = "OFF"
  7.  End If
  8. End Sub
図4-5

ボタンの表示は「ON」か「OFF」の二択です。「Color_Event」フラグが True であれば「計測が作動している」事になりますので「ON」に、その逆の停止時は「OFF」にします。

ここで雑談ですが、シート上にボタンを作る方法は色々存在します(図4-6参照)。代表的なものは1)と2)ですが、3)も視覚的に効果があると思いますし、4)も自由度が高くマクロも登録できますので面白いと思います。
1)「フォームコントロール」で作る方法
2)「ActiveXコントロール」の「コマンドボタン」で作る方法
3)「ActiveXコントロール」の「トグルボタン」で作る方法
4)「図形(オートシェイプ)」の四角形等で作る方法


図4-6

今回のサンプルでは1)のフォームコントロールでボタンを作ってあります。これは、シート上のボタンに標準モジュールで作ったプロシージャを登録させる為には「フォームコントロール」か「図形」である必要があるからです。
なお「ActiveXコントロール」での登録マクロは「イベント」っぽい命名で、作ったシートのシートモジュール上に作られます。
1)~4)まで、それぞれ長所短所がありますので、用途を考えながら使って下さい。

また、ボタンや図形をVBAで使おうとした時、その作ったもの(Shape)の名前が分からないとコードが書けません。そういう時私は「マクロ自動記録」を使い、適当にそのShapeを動かしたりして出来た自動生成コードから名前をコピペして使っています。
(都度 Selection.Name とマクロを組むのも面倒なので・・)

4-4.セル色情報の取得

セルの色情報を取得し、配列に格納するのが図4-7の「Cell_Color」プロシージャです。
  1. '========== ⇩④ セル色情報の取得 =====================
  2. Private Sub Cell_Color()   '←色情報取得
  3.  Dim C As Range
  4.  For Each C In R       '←計測範囲Rのセルを1つずつ調べる
  5.   RA(C.Row, C.Column) = C.Interior.Color   '←配列に色番号を代入
  6.  Next C
  7. End Sub
図4-7

計測範囲R内のセルを1つずつ取り出し(30行目)て、そのセル色情報を31行目で配列に収めます。=の右辺は「1つ1つのセルのセル色番号」で、=の左辺は配列 RA です。RAの各要素として直接「(C.Row, C.Column)」を指定できるように、図4-3の15行目で「ReDim RA(R(1).Row To R(R.Count).Row, R(1).Column To R(R.Count).Column)」と、ちょっと変わった配列要素の指定をしました。
配列インデックスは0から(又は1から)が基本 との考え方もありますが、「分かり易さ」で今回は変則的な方法を取っています。

尚、ユーザが操作に使える時間を少しでも増やそうとして「For~Nextの間にDoEventsを入れる」ことを考える方もおられると思います。しかし、ボタンを高速連打の様な操作をすると、For~Nextの途中で処理が終わってしまい中途半端な色情報配列(途中からデータが無い配列)となる現象が発生します(タイミングにもよりますが)。
こうなると、比較元が無い状態で比較が行われるので、次々に「色が変更されました」のコメントが発生することになります。もちろん連打する人が悪いのですが、そういう人もいる事を忘れずに、「必要な処理は完結させる」事を優先して途中にDoEventsを差し込むのは止めましょう。


4-5.一定時間ごとに繰り返し監視する

セル色情報を取得出来た後で、ユーザがセル色を変更するのを定期的にチェックするのが図4-8の「Timer」です。
  1. '========== ⇩⑤ 一定時間ごとに繰り返し監視する ======================
  2. Private Sub Timer()
  3.  If Color_Event = False Then Exit Sub   '←計測停止状態なら抜ける
  4.  'Call Color_Comp3             '←色データ比較しコメント出す3(Array型)
  5.  'Call Color_Comp2             '←色データ比較しコメント出す2(Range型)
  6.  Call Color_Comp1             '←色データ比較しコメント出す1(単発型)
  7.  Application.OnTime Now() + TimeValue("00:00:01"), "Timer"
  8. End Sub
図4-8

36行目で「Color_Event」フラグがFalseの時に停止させるコードが入っていますが、これについては後で説明します。

37~39行目では「Color_Comp1」「Color_Comp2」「Color_Comp3」の内、どれかを呼び出します。
3つの違いは以下の通りです。(どれを使うかは、使用場面により選んで下さい)
尚、出力方式が異なるため、Color_Comp プロシージャ と Msg プロシージャはセットとなります。

「1」:変更セル情報は単発型・・セル情報(Range型)を単独で返す。複数セル変更時は複数回返す
「2」;変更セル情報は範囲型・・Worksheet_Changeイベント等の引数のようなRange範囲を返す
「3」;変更セル情報は配列型・・配列の中にセル情報(Range型)を格納して返す

呼び出した「Color_Comp」内で、初期値色情報(Cell_Colorで取得したもの)と現時点でのセル色とを比較し、もし異なっているセルがあればメッセージを出し、同時に初期値を更新します。

その後40行目の「Application.OnTime」で、実行プロシージャの予約をします。
「OnTime」の第1引数は「実行時刻」ですが、現時点を基準とする時は「Now() + TimeValue("00:00:01")」のようにTimeValue関数を使用します。図4-8の40行目は「現在の1秒後」を表しています。

「OnTime」の第2引数は「実行するプロシージャ」を「文字列として(=両端をダブルクォーテーションで囲む)」指定します。今回実行するのは「Timerプロシージャ」で自分自身ですので、「ある一定時間ごとに同じTimerプロシージャが動作」することになります。Timerプロシージャでは、1つ前のTimerプロシージャで保存したセル色情報と現在のセル色を比較しコメントを出す機能を持っていますので、「常にセル色を監視」していることになるのです。

尚、OnTimeで実行できるプロシージャは「標準モジュール」に記述されたもののみです。シートモジュール等のプロシージャを指定してもエラーが発生しますのでご注意下さい。(そのため、本マクロも標準モジュールに記述しています)

先延ばしにした36行目の「Color_Event」がFalseの時の処理についてです。
「Timerプロシージャ」は一定時間ごとに実行されていますので、ユーザが操作できるのは、その実行と実行の間(図4-9の④)のみになります。つまり、④の間でのみユーザがSTOP(ボタンを押す)が出来ることになります。
しかしユーザがSTOPさせた時には、その前の③のOnTime処理は終わっており、⑤のTimerプロシージャ実行の予約は完了しています。ですから、STOPを指示しても一定時間後にはTimerプロシージャが始まってしまいますし、その後もずっとTimerプロシージャ予約は続き「Stopを掛けても、止まらない」事になります。
ですので、Stopを指示したフラグ(Color_Event = False)でTimerプロシージャを途中で終了させているのです。

図4-9

尚、OnTimeについては「再計算されたか否かのチェックをイベントで取得」も参照下さい。

4-6.セル色情報の比較・情報修正・コメント出し 1

まず最初に「変更セル情報は単発型」の処理について説明します。

図4-10「Color_Comp1」プロシージャ内でセル色比較をし、色情報修正(Cell_Color_Part)とメッセージ(Msg1)を呼び出しています。
  1. '========== ⇩⑥ セル色情報の比較1(単発型) ==================
  2. Private Sub Color_Comp1()     '←色情報比較
  3.  Dim C As Range
  4.  For Each C In R         '←計測範囲Rのセルを1つずつ調べる
  5.                  '↓保存してある色情報と異なる時
  6.   If Not RA(C.Row, C.Column) = C.Interior.Color Then
  7.    Call Cell_Color_Part(C)   '←部分的に色情報取得
  8.    Call Msg1 (C)         '←メッセージを呼び出す
  9.   End If
  10.  Next C
  11. End Sub
図4-10

45行目で範囲R内のセルを1つずつ調べ、47行目で「セル色情報の配列」の値と実際のセル色を比較し、(Not  がついているので)異なれば、色情報修正とメッセージを出しています。
複数のセルでセル色が異なっていた場合には、1つ違うたびに「修正+コメント」をセットで出しています。

ちなみに、この「Msg1 (C) 」が「セル色変更イベント」相当という事になります。


続いて、Color_Comp1から48行目で呼び出された「Cell_Color_Part」プロシージャが図4-11になります。
  1. '========== ⇩⑦ 部分的に色情報取得・修正 ==============
  2. Private Sub Cell_Color_Part(C As Range)    '←部分的に色情報取得・修正
  3.  RA(C.Row, C.Column) = C.Interior.Color   '←異なっていた部分のみ更新
  4. End Sub
図4-11

引数として「変更された単一セル範囲(Range型)」を受け取ります。
図4-7の「Cell_Color」プロシージャで配列RAにセル色一覧を格納していますが、その配列RAを元にして「色が変更されたセル」だけの色番号を修正しています。
この「Cell_Color_Part」プロシージャの代わりに「Cell_Color」プロシージャをもう一度動かしても良いのですが、計測範囲が広くなるとその分処理時間が多くかかりますので「異なる部分だけ修正」の方が効率的です。


セル色修正をしたのち、図4-10の49行目で呼び出されるのが図4-12の「Msg1」プロシージャになります。
  1. '========== ⇩⑧ メッセージ発行1(単発型) ===================
  2. Private Sub Msg1(C As Range)       '←メッセーシ発行
  3.  MsgBox C.Address & "のセル色が" & vbCrLf & "色番号:" _
  4.   & C.Interior.Color & vbCrLf & "に変更されました"
  5. End Sub
図4-12

引数としては Cell_Color_Part と同じく、「色が変更された単一セル」を受け取ります。
59~60行目は「 _ 」で繋がっていますので、一つの行です。メッセージ(MsgBox)として「変更されたセルのアドレス」・「変更されたセルの色番号」を表示させています。

なお「vbCrLf」という定数は改行を表しています。実はExcelでは「vbLf」「vbCrLf」「vbNewLine」のどれでも改行になりますが、図4-13の通り特徴があります。

 vbLf  Excel専用の改行 
 vbCrLf  Windows一般の改行 
 vbNewLine  O/Sに合わせて自動的に適切な改行をする 
図4-13

私も最近「vbNewLine」の存在を知ったのですが、この中では「vbNewLine」が最も良さそうですね。


4-7.セル色情報の比較・情報修正・コメント出し 2

次は「変更セル情報はRange範囲型」です。通常のWorksheet_Changeイベント等の引数「Target」の様にセル範囲を返します。
図4-14の「Color_Comp2」プロシージャ内でセル色比較をし、色情報修正とメッセージ(Msg2)を呼び出しています。なお、色情報修正(Cell_Color_Part)は図4-11と同一(他の方式と一緒の処理)です。
  1. '========== ⇩⑨ セル色情報比較2(Range型) ===================
  2. Private Sub Color_Comp2()     '←色情報比較
  3.  Dim C As Range          '←計測範囲の1セル
  4.  Dim C_Union As Range      '←色変更されたセルの集合
  5.  For Each C In R         '←計測範囲Rのセルを1つずつ調べる
  6.                  '↓保存してある色情報と異なる時
  7.   If Not RA(C.Row, C.Column) = C.Interior.Color Then
  8.    Call Cell_Color_Part(C)    '←部分的に色情報取得
  9.    If C_Union Is Nothing Then  '←C_Unionが空だったら
  10.     Set C_Union = C       '←C_Unionに1つ目のセルを代入
  11.    Else
  12.     Set C_Union = Union(C_Union, C)   '←C_Unionに2つ目以降のセルを結合
  13.    End If
  14.   End If
  15.  Next C
  16.  If Not C_Union Is Nothing Then Call Msg2(C_Union)   '←メッセージを呼び出す
  17. End Sub
図4-14

64~65行目はプロシージャ内で使用する変数の宣言で、65行目の「C_Union」は、複数セルの変更を想定したセル範囲です。(宣言上は単一セルと変わりません)

66行目で設定範囲の1つ1つのセルを調べていきます。
68行目で、元のセル色情報配列RAと調べるセル色を比較し異なっている場合は、69行目で色情報修正をします。ここまでは4-6項と同じですが、その先の70~74行目でセル範囲を結合していきます。
まず、1個目のセル(=色が異なっているセル)が来た時ですが、結合するもとのセル範囲「C_Union」は宣言しただけの状態なので空(Nothing)です。「Unionメソッドは、空と実体のセルを結合することは出来ません」ので、空の時(C_Union Is Nothing)は別な操作が必要となります。
空の時(70行目)は変数 C_Union にそのままセル情報を代入(71行目)し、空で無い時(2個目以降の時:72行目)はUnionを使って、それまでに結合したセル範囲と新しいセル範囲を結合していきます。
最後まで調べ切った(For Each Nextを抜けた)ら、77行目でメッセージを出すプロシージャにセル範囲を引数につけて呼び出します。
なお77行目で、メッセージを呼び出す際に「If Not C_Union Is Nothing Then」とIF文で選別しているのは、もし変更したセルが無かった場合に無駄な処理をしない為です。Msg2プロシージャ側で引数チェックをする方法もあるかと思いますが、「どうせメッセージしないのだから、呼び出すだけ無駄」と考え、77行目で選別する事にしました。


図4-14の77行目から呼び出されるプロシージャ「Msg2」が図4-15です。
  1. '========== ⇩⑩ メッセージ発行2(Range型) ======================
  2. Private Sub Msg2(C As Range)   '←メッセージ呼び出し
  3.  Dim Str_C As String
  4.  Dim C_i As Range
  5.  For Each C_i In C
  6.   Str_C = Str_C & C_i.Address & " & "   '←セル位置を&でつなぐ
  7.  Next C_i
  8.  Str_C = Left(Str_C, Len(Str_C) - 3)    '←最後の&を消す
  9.  MsgBox Str_C & vbCrLf & "のセル色が変更されました"
  10. End Sub
図4-15

Msg2プロシージャが受け取る引数 C は、セル範囲を示すRange型です。一般のWorksheet_Changeイベント等で受け取るTarget As Range(セル範囲)と同様のものです。
81~82行目はプロシージャ内で使用する変数の宣言です。引数として複数セル範囲が来る可能性があり、メッセージとしてセル範囲アドレスを連記するために81行目の「Str_C」変数をString型で宣言しています。

84行目で複数セル範囲を1セルずつ調べ、85行目でセルのアドレスを「&」でつないでいきます。
For Next を抜けた後、87行目で一番後ろについている余計な「&」を取り除きまず。今回は「 _&_ 」と両端にスペースを入れていますので、87行目の数式で3文字分を削除しています。
そして89行目でメッセージボックスを表示させています。


4-8.セル色情報の比較・情報修正・コメント出し 3

最後に「変更セル情報はArray型」です。セル範囲を配列に格納して返すものです。
図4-16の「Color_Comp3」プロシージャ内でセル色比較をし、色情報修正とメッセージ(Msg2)を呼び出しています。なお、色情報修正(Cell_Color_Part)は図4-11と同一(他の方式と一緒)です。
  1. '========== ⇩⑪ セル色情報比較3(Array型) =======================
  2. Private Sub Color_Comp3()       '←色情報比較
  3.  Dim C As Range            '←計測範囲の1セル
  4.  Dim C_Array() As Range        '←色変更されたセルの集合
  5.  Dim i As Long             '←カウンター変数(=変更箇所数)
  6.  For Each C In R            '←計測範囲Rのセルを1つずつ調べる
  7.                     '↓保存してある色情報と異なる時
  8.   If Not RA(C.Row, C.Column) = C.Interior.Color Then
  9.    i = i + 1
  10.    Call Cell_Color_Part(C)      '←部分的に色情報取得
  11.    ReDim Preserve C_Array(1 To i)  '←配列サイズ変更
  12.    Set C_Array(i) = C         '←C_Array配列に対象セルを代入
  13.   End If
  14.  Next C
  15.  If Not i = 0 Then Call Msg3(C_Array)  '←メッセージを呼び出す
  16. End Sub
図4-16

93~95行目はプロシージャ内で使用する変数宣言です。このプロシージャではセル範囲を配列型にしますので、94行目は「Dim C_Array() As Range」と動的配列として宣言をしています。
96行目で計測範囲のセルを1セルずつ調べ、98行目で元のセル色情報配列RAと調べるセル色を比較し異なっている場合、まずは100行目でセル色情報修正をしたのち、101行目で「色変更のセルを格納する配列(C_Array)」のサイズをReDimで大きくします。
102行目では、サイズを大きくしたC_Array配列の最後の要素(i)にセル範囲を入れていきます。

計測範囲のセル調査が全て終わった段階で、配列C_Arrayには変更のあった全セルが格納されていますので、105行目でメッセージを呼び出します。
105行目で「If Not i = 0 Then ・・・」となっていますが、変更のあったセルの個数は99行目の「i = i + 1」で数えています。その「i」がゼロだったら「変更したセルは無かった」事になりますのでメッセージを出す必要が無いことになります。


図4-16の105行目で呼び出すプロシージャ「Msg3」が図4-17です。
  1. '========== ⇩⑫ メッセージ発行3(Array型) ============
  2. Private Sub Msg3(C() As Range)         '←メッセージ呼び出し
  3.  Dim Str_C As String
  4.  Dim i As Long
  5.  For i = LBound(C, 1) To UBound(C, 1)    '←配列の各要素を順に取り出す
  6.   Str_C = Str_C & C(i).Address & " & "   '←セル位置を&でつなぐ
  7.  Next i
  8.  Str_C = Left(Str_C, Len(Str_C) - 3)    '←最後の&を消す
  9.  MsgBox Str_C & vbCrLf & "のセル色が変更されました"
  10. End Sub
図4-17

Msg3プロシージャが受け取る引数 C( ) は、セル範囲が格納されている配列です。配列ですので両カッコがついています。
109~110行目はプロシージャ内で使用する変数宣言です。

配列の中に収められているセル情報を先頭から最後尾まで調べるため、112行目の様にFor ~ Next で1つずつ取り出して行きます。その際の「先頭(最小インデックス)」は「LBound」で、「最後尾(最大インデックス)」は「UBound」で取得するのが一般的です。但し先頭は「1」と決まっているのであれば、「For i = 1 to ・・・」でも良いかと思います。
またLBound関数・UBound関数の第二引数の「1」ですが、今回は一次元の配列ですので、一次元の「1」を指定しています。

113行目では、セルのアドレスを「&」でつなげていき、全て調べ終わった(For ~ Next を抜けた)ら、115行目で最後尾の余分な「 & 」を取り除きます。
そして117行目でメッセージとして表示をします。



5.最後に

今回、「セル色変更時のイベント」のようなものを作ってはみましたが、マウス形状が1秒ごとにビジー状態になるのは、やはり気になります。
変更状態をリアルタイムまでして取得する必要があるのであれば我慢しますが、そうでないならブックを閉じる時とかシートを抜ける時とかのイベントで引っ掛けて検査する、という方が現実的と思います。

また「ActiveXコントロール」のボタンの話が出てきましたが、ブックの保存・開くを繰り返しているとボタン(他のコントロールだったかもしれません)のサイズが変わってしまう不具合を昔経験したことがあります。仕方ないのでWorkbook_Openでコントロールの大きさを毎回元のサイズに戻すコードを入れていました。
他のサイトでも同様の不具合が語られているようなので、注意した方が良いと思います。


セル色変更の情報をイベント風に受け取る(it-020.xlsm)

セキュリティ向上を目的として「インターネット経由でダウンロードしたOfficeファイル(Excel等)のマクロは、既定でブロック」されるようにOfficeアプリケーションの既定動作が変更になりました。(2022年4月より切替開始)
解除の方法については「ダウンロードファイルのブロック解除方法」を参照下さい。