2021/06/18

シート上の図形の代替テキストを閲覧・編集




1.背景

ワークシート上に配置した図形(シェイプ)には、図2-1のように「代替テキスト」を設定することが可能です。今までも、その機能を使ったシステム(例えば「両矢印線の図形を日程線としてセル上に描画」)を考案したことはありました。しかし今回は、文字通り「図形に説明文を付ける」または「図形の説明文を見る」というシステムを考えましたので、紹介します。

用途として頭に思い描いているのは、
 ・日程表(図罫線や箱型矢印を使って日程線を引くもの)の、日程線そのものに「業務結果や問題点」などを記入
 ・写真などをシートに貼り付け、その情報をテキストとして写真内に保存・閲覧
のようなものです。もっと面白い使い方もあるだろうと思います。

2.システム概要

図2-1のように、シート上に配置した図形の書式設定の1つに「代替テキスト」と言うのがあります。もともとは「作成した図表・画像・その他のオブジェクト内の情報をテキストで表した代替情報」で、「オブジェクトを見たり理解するのが困難な視覚障碍や認識障碍のある方に」対して読み上げる機能のあるアプリもあるようです。
図形の代替テキスト
図2-1

テキストとしての制限については、どこにも記述が見当たりませんでしたが、ある程度の文字数(試したところ、全角10.000文字は入りました)は保存できそうです。
今回システムでは、この「代替テキストの説明」部分を利用しています。

まず、アドイン等からシステムを起動すると、図2-2のような「システムON-OFF」用のダイアログが画面左上角に表示されます。
システムの起動
図2-2

「システムON-OFF」ダイアログの「停止中」というトグルボタンをクリックすると「起動中」に変わり、システムが起動状態になります。その状態でマウスをシート上の図形の上に重ねると、図2-3のように「新たなダイアログ(UserForm2)」が表示されます。
マウスを図形に載せると代替テキストを表示
図2-3

新たなダイアログにはテキストボックスがあり、そこに図形が持つ「代替テキスト」の文字列が表示されます。
なお、マウスが図形(及び表示されたダイアログの範囲)から外れれば、図2-4のように表示されていたダイアログは自動的に消えます。
図形からマウスが外れるとダイアログ消去
図2-4

また、表示されたダイアログ内のテキストボックスを編集し、図2-5のように「保存」ボタンをクリックすれば、編集した内容が図形の代替テキストとして保存されます。
代替テキストの編集も可
図2-5

なお、図形または表示ダイアログからマウスが外れてしまうと表示ダイアログが消えてしまいますので、「編集した後、保存ボタンをクリック」するには少しコツが必要かもしれません。
また図形が重なっている場合は、狙った図形とは異なる図形の代替テキストがダイアログに表示される場合があります。理由は、シート内の図形をFor Each~Nextで次々に確認し、マウスの位置と丁度タイミングが合致した図形に対して反応させるようにしている為です。

なおシステムの起動を中断するには、左上ダイアログの「起動中」ボタンをクリックし、「停止中」の状態にします。またシステムを終了するには「終了」ボタンをクリックすることで、左上ダイアログは消去します。
再度システムを起動させるには、アドインボタン等をクリックする必要があります。

3.プログラムの流れ

システム起動中は図3-1の一番左側のように、Do~Loop と For Each~Next で「シート上の全ての図形の位置とマウスの位置が重なっているか否か」を常時調べています。そして、重なっている(=図形の上にマウスが乗る)と判断されるとフォーム(今回はUserForm2)を起動すると共に、図3-1の一番右側のループに移動します。そのループでは「重なった図形のみにFocus」を当てた状態で、「マウスが図形及び起動したダイアログ上に居るか」をDo~Loopで確認し続けます。
プログラムの流れ
図3-1

起動したダイアログでは、Focusした図形の代替テキストをフォーム上のTextBoxに書き込みます。またユーザーがダイアログを移動することも考えられるため、移動した際(Layoutイベントを使用)にはその新しいダイアログ位置をマウスとの重なり確認のDo~Loopに情報提供します。
また、ユーザーがダイアログ内TextBoxを編集し「保存」ボタンをクリックした際には、TextBoxの内容を図形の代替テキストに書き込みます。

Focusした図形及びダイアログからマウスが外れると、制御は図3-1の右側から左側へ戻されると同時に、ダイアログを削除します。

4.標準モジュール1(Module1)

標準モジュールは、2つに分けてあります。Module1には「システムの起動」及び「図形・ダイアログの位置とマウスの位置の関係」を調べ、「代替テキストを表示するダイアログを起動・削除」するメインプログラムを置いています。
Module2には「ディスプレイ拡大率」を取得するコードを別置きしています。

4-1.変数宣言、外部プロシージャ参照宣言など

Module1の宣言部では、システム内で共通使用する変数の宣言、及び外部プロシージャ(マウスの位置取得)参照宣言を行っています。
  1. '========== ⇩(1) 変数宣言、外部プロシージャ参照宣言など ============
  2. Public UFleft As Long        '←UserForm2の左端位置(ピクセル)
  3. Public UFtop As Long        '←UserForm2の上端位置(ピクセル)
  4. Public UFright As Long       '←UserForm2の右端位置(ピクセル)
  5. Public UFbottom As Long      '←UserForm2の下端位置(ピクセル)
  6. Public WinScreenRatio As Double   '←ディスプレイ拡大率
  7. Public SystemStop As Boolean    '←システムをStopさせるフラグ
  8. Private Type POINTAPI        '←マウスの位置の構造体
  9.  X As Long
  10.  Y As Long
  11. End Type
  12. Declare Function GetCursorPos Lib "user32" (IpPoint As POINTAPI) As Long
図4-1

2~5行目の変数は、図形の代替テキスト内容を表示するダイアログ(UserForm2)の位置を入れる変数です。位置は図4-2のように、ダイアログのディスプレイ原点からの距離をピクセル単位で入力します。
この変数に値を入れる工程は、UserForm2の中の「Layoutイベント」プロシージャと「Terminateイベント」プロシージャ(図7-5)です。
ダイアログ位置変数の定義
図4-2

位置関係は通常「Left・Top・Width・Height」で表しますが、今回は「マウスの位置が図形やダイアログの上に重なっているか」がメインですので、数式を簡単にするために「Left・Top・Right・Bottom」で表すことにしました。

また、マウス・図形・ダイアログの「座標系や単位」は、図4-3のようにバラバラです。スクリーン座標⇔ドキュメント座標の変換、またピクセル⇔ポイントの変換はもちろん可能ですが、「スクリーン座標 × 単位ピクセル」に統一する事で「マウスの位置は計算不要」となります。別な言い方をすれば「図形とダイアログの計算式に集中できる」ことになります。
座標系単位
マウススクリーン座標ピクセル
図形ドキュメント座標ポイント
フォーム位置スクリーン座標ポイント
図4-3

7行目の「WinScreenRatio」は、ディスプレイ拡大率です。手動では図4-4のように、Windowsの設定→システム→ディスプレイ→拡大縮小とレイアウト から設定できます(選択出来る種類は機種によります)。
ディスプレイ拡大率
図4-4

この拡大率をVBAで取得するには、まずはModule2のようなWindowsAPIを使って「ディスプレイ拡大率込み」の「dpi(dots per inch:1インチの幅の中にどれだけのドットを表現できるか」を取得し、その値を標準値の96で割る事で「ディスプレイ拡大率」を計算します。今回はこの割り算の結果を変数WinScreenRatioに代入(図4-8の34行目)しています。

8行目の「SystemStop」は、処理を途中で停止させるためのフラグです。今回システムでは、マウスと図形・ダイアログの重なりをDo~Loopで常に監視していますので、そのDo~Loopの脱出条件の1つにSystemStopフラグを加え、フラグがON(True)になった時にDo~Loopを抜け出す(=システム中断)ようにしています。

10~13行目の「POINTAPI」は、マウス位置座標用の構造体を宣言しています。15行目の「GetCursorPos」では、マウス位置に相当するスクリーン座標を出力してきますので、それを受け取る箱の役目をします。
11行目の「X」、12行目の「Y」は、図4-5のような方向になります。
ディスプレイのX方向Y方向
図4-5

15行目の「Declare Function GetCursorPos Lib "user32" (IpPoint As POINTAPI) As Long」では、Declare文を使ってWindowsAPIのGetCursorPosを宣言しています。出力先は10~13行目で宣言した「POINTAPI」を指定しています。

4-2.システムON-OFFダイアログの表示プロシージャ

システムの準備(システム起動ON-OFFスイッチのダイアログ表示:UserForm1)は、図4-6の「SystemStartプロシージャ」から行います。アドインに登録をした場合の実行マクロは、この「SystemStart」を指定して下さい。
  1. '========== ⇩(2) システムON-OFFダイアログ表示 ============
  2. Public Sub SystemStart()
  3.  UserForm1.StartUpPosition = 3
  4.  UserForm1.Show 0
  5. End Sub
図4-6

19行目の「UserForm1.StartUpPosition = 3」では、UserForm1の表示位置を「画面の左上角」に指定しています。StartUpPositionには図4-7のように4種類の設定が可能です。
なお、UserForm2(図形の代替テキストを表示・編集するダイアログ)を表示する際(図7-2の138行目)は、「Focusした図形の左上角に合わせる」ようにするため、値0を使っています。 ちなみに既定値は「1」のようで、StartUpPositionを指定せずにShowメソッドを実行すると「Excelの中央」に表示されます。
説明
0初期設定値を指定せず(マクロで指定可)
1UserFormが属する項目(Excelドキュメント)の中央(既定?)
2画面全体の中央
3画面の左上隅
図4-7

20行目の「UserForm1.Show 0」では、モードレス(ダイアログ以外の操作が可能)でUserForm1を表示させています。

4-3.マウスと図形等の位置関係チェック

システムON-OFFダイアログ(UserForm1)上のボタンをクリックして「起動中」にすると、図4-8のSPsearchプロシージャが呼び出されます。
  1. '========== ⇩(3) マウスと図形等の位置関係チェック ============
  2. Public Sub SPsearch()
  3.  Dim p As POINTAPI    '←POINTAPI型での変数を宣言
  4.  Dim s As Shape       '←図形1つ1つの変数
  5.  Dim LK As Boolean     '←マウスが重なった図形からFocusを外さない様にするフラグ(Lockの意)
  6.  Dim SPleft As Long     '←図形の左端位置
  7.  Dim SPright As Long     '←図形の右端位置
  8.  Dim SPtop As Long      '←図形の上端位置
  9.  Dim SPbottom As Long    '←図形の下端位置
  10.  WinScreenRatio = LogicalPixcel / 96
  11.  Do
  12.   For Each s In ActiveSheet.Shapes
  13.    Do
  14.     SPleft = ActiveWindow.PointsToScreenPixelsX(0)
  15.          + (s.Left / 0.75) * (ActiveWindow.Zoom / 100) * WinScreenRatio
  16.     SPright = ActiveWindow.PointsToScreenPixelsX(0)
  17.          + ((s.Left + s.Width) / 0.75) * (ActiveWindow.Zoom / 100) * WinScreenRatio
  18.     SPtop = ActiveWindow.PointsToScreenPixelsY(0)
  19.          + (s.Top / 0.75) * (ActiveWindow.Zoom / 100) * WinScreenRatio
  20.     SPbottom = ActiveWindow.PointsToScreenPixelsY(0)
  21.          + ((s.Top + s.Height) / 0.75) * (ActiveWindow.Zoom / 100) * WinScreenRatio
  22.     GetCursorPos p
  23.     If (SPleft <= p.X And SPright >= p.X And SPtop <= p.Y And SPbottom >= p.Y) Or _
  24.       (UFleft <= p.X And UFright >= p.X And UFtop <= p.Y And UFbottom >= p.Y) Then
  25.      LK = True
  26.      Call UserForm2.UFstart(s)
  27.     Else
  28.      LK = False
  29.      If UserForm2.Visible = True Then Unload UserForm2
  30.     End If
  31.     DoEvents: DoEvents
  32.    Loop While LK = True And SystemStop = False
  33.   Next s
  34.   DoEvents: DoEvents
  35.  Loop While SystemStop = False
  36. End Sub
図4-8

34行目の「WinScreenRatio = LogicalPixcel / 96」では、ユーザー定義関数「LogicalPixcel(図5-3)」を呼び出し「ディスプレイ拡大率込みのdpi値」を取得し、それを標準値「96」で割ることで「ディスプレイ拡大率」を算出し、変数WinScreenRatioに代入しています。

36~66行目のDo~Loop(外側のDo~Loop)の繰返し条件は66行目の「Loop While SystemStop = False」ですので、ストップが掛かる(=フラグSystemStopがTrueになる)まで、マウスの位置と図形の位置とを比較し続けます。
外側Do~Loop内では、37行目の「For Each s In ActiveSheet.Shapes」で「シート内の図形を1つずつ調査」しています。
プログラムの流れで言うと、図3-1の左側のループになります。

For Each~Nextの内側には、39~62行目のDo~Loop(内側のDo~Loop)があります。この繰返し条件は62行目の「Loop While LK = True And SystemStop = False」で、「フラグLKがTrue」かつ「フラグSystemStopがFalse」です。
この「フラグLK」は、「図形とマウスが重なっている(52~53行目のIf文が成立した)」ときにTrueになる(54行目)ようにしており、また「重なりが外れた(52~53行目のIf文が成立しない)」ときにはFalse(57行目)にしています。

つまり、マウスと図形が重なっていない時は「外側のDo~Loopを回しながらシート内の全ての図形を調べ、内側のDo~Loop内は1回だけ実行」され、マウスと図形が重なっている間は「1つの図形に対して、内側のDo~Loop内だけを実行し続ける」ことになります。
この二重ループのコード構造にすることで、「マウスと図形の重なりを調べるIf文を1つ」にすることが出来、また「1つの図形に対してダイアログを表示している間は、他の図形を調べに行かない(=ダイアログが開いたり閉じたりしない。テキストの編集が可能)」ことが可能になります。
この内側Do~Loopの部分がプログラムの流れで言うと、図3-1の右側のループになります。

内側のDo~Loop内では、41~48行目で「シート上の図形の1つ」である「s」の位置を計算します。位置はダイアログの位置(図4-2)と同様に、図4-9のように「Left・Top・Right・Bottom」で表すことにします。
図形の位置
図4-9

「Left・Top・Right・Bottom」の4種類の位置の式がありますが、代表して41~42行目のLeft方向の位置「SPleft」の式で説明していきます。
Left方向の位置「SPleft」の式は「SPleft = ActiveWindow.PointsToScreenPixelsX(0) + (s.Left / 0.75) * (ActiveWindow.Zoom / 100) * WinScreenRatio」です。
右辺の前半の「ActiveWindow.PointsToScreenPixelsX(0)」は、図4-10のように「ドキュメント座標の原点の位置を、スクリーン座標として取得」するものです。41・43行目はX方向なので「ActiveWindow.PointsToScreenPixelsX(0)」を使い、45・47行目はY方向なので「ActiveWindow.PointsToScreenPixelsY(0)」を使っています。
PointsToScreenPixelsXメソッドのカッコ内(引数)には「ドキュメント座標の原点」を表す「0(ゼロ)」を指定しています。なお、このメソッドで得られる距離は「ピクセル」単位です。
ドキュメント座標の原点位置の取得
図4-10

次にその後ろの「+ (s.Left / 0.75)」は、「ドキュメント座標原点から、図形左端までの距離」を表しています。通常図形の位置取得・設定として使用している「s.Left」などは、図4-11の左側のように「ポイント」単位です。
今回、距離についてはマウス位置の単位である「ピクセル」に合わせていきます。ポイントとピクセルの関係は「72ポイント=96ピクセル」ですので、ポイント値をその係数「0.75」で割ることでピクセル単位に変換できます(図4-11の右側)。
ドキュメント座標原点から図形までの距離の取得
図4-11

よって、図形の左端位置を算出するには「SPleft = ActiveWindow.PointsToScreenPixelsX(0) + (s.Left / 0.75) 」となるのですが、Excelは表示倍率の変更が可能です。図4-12の左側は100%の表示倍率、右側は50%の表示倍率です。
Excelの画面拡大縮小による影響
図4-12

Excelの表示倍率は「ActiveWindow.Zoom」プロパティで取得できます。100%の時に「100」、50%の時は「50」の値が得られますので、先程のドキュメント座標上の図形のピクセル単位の位置(s.Left / 0.75)に「ActiveWindow.Zoom / 100」を掛ければ良いことになります。

また、図4-4に示したように「ディスプレイ拡大率」もユーザーが設定できる項目です。切り替えると図4-13のようにディスプレイ上の各アプリのサイズが変わります。ですので、この「ディスプレイ拡大率」も更に掛け合わせることが必要になります。
なお「ディスプレイ拡大率」は、図4-8の34行目の「WinScreenRatio = LogicalPixcel / 96」で計算済みで、変数WinScreenRatioに代入されています。
Excelの画面拡大縮小による影響
図4-13

これらを全て盛り込むことで、「ディスプレイ拡大率とExcel表示倍率を考慮した図形位置」が求まります。
ちなみに、ドキュメント座標原点の位置である「PointsToScreenPixelsX(0)」「PointsToScreenPixelsY(0)」は、Excel表示倍率やディスプレイ拡大率の変更に合わせた値となっています(言わば、表示倍率・拡大率を含んだ原点)。
ですので「PointsToScreenPixelsX(0)」「PointsToScreenPixelsY(0)」には、表示倍率・拡大率は掛け合わせません。

Left以外の「Top・Right・Bottom」の式も同様の考え方です。なお「Right」は「s.Left + s.Width」、「Bottom」は「s.Top + s.Height」となります。なお、各値とも単位はピクセルです。

寄り道
例えば41~42行目の図形位置の計算式では「ActiveWindow.PointsToScreenPixelsX(0)」を使っています。
この「PointsToScreenPixelsXメソッド(Yについても同様)」について、「Microsoftサイト1」を含む他のサイトでは、「PointsToScreenPixelsXメソッド」のカッコ内(引数)に「変換する横方向の長さをドキュメント座標の左端を基点としたポイント単位の値で指定」する事で、「スクリーン座標のピクセルに変換」するという様に説明しています。

どう読んでも「引数には、ポイント単位で入力」としか思えないのですが、一方で言い訳にも読み取れる説明が「Microsoftサイト2」にあるように、実際には「ドキュメント座標のピクセル単位で入力」する必要があるので注意が必要です。

ですので、例えば今回の41~42行目を「SPleft = ActiveWindow.PointsToScreenPixelsX(s.Left * (ActiveWindow.Zoom / 100) * WinScreenRatio)」と記述すると、引数がポイント単位のため、間違った値が返ってくることになります。
正しくは「SPleft = ActiveWindow.PointsToScreenPixelsX(( s.Left/ 0.75) * (ActiveWindow.Zoom / 100) * WinScreenRatio)」と、「075で割って、ピクセル値に変換」する必要があります。

なお「0ピクセル=0ポイント」なので、ドキュメント座標の原点は「PointsToScreenPixelsX(0)」で取得できます。どっちだったか自信が無い場合は、まずは「ゼロ」で原点を求めてから「ポイントなのかピクセルなのかを考え」ながら式を立てる方が間違えないかもしれません。

寄り道
ポイント⇔ピクセル変換」について、ここで整理しておきまます(以下は、Windowsでの話です)。
ここまで、ポイントをピクセルに変換するには「0.75で割る」と説明してきましたが、この変換係数は以下の関係から導いています。
 1ピクセル=1/96インチ
 1ポイント=1/72インチ
72/96 = 3/4 = 0.75 というのが変換係数です。

まず「ピクセル」の方を詳しく見ていきます。ピクセルは「ディスプレイのドット」で、1インチ当たりのドット数を「dpi(dot per inch)」として表します。上の式は「96ドット/インチ」という事を示していることになります。
この値は画面の寸法と画面の解像度から測定出来ますので、試しに私のノートPCで測ってみると、図4-14のように「約148dpi」となります。
実際のPCのDPI値
図4-14

そう「ディスプレイは、96dpiでは無い」のです(96dpiのディスプレイも存在するとは思います)。
では「1ピクセル=1/96インチ」とは何かと言うと、正確には「96ドットを論理インチとする」と言うことなのです。この背景としては、世の中の様々な大きさのディスプレイに対応するためには、このような論理値として扱うしかなかった ようです。

次に「ポイント」です。ポイントは元々は印刷の世界の単位で、文字の大きさを表します。コンピュータで文字サイズを扱う時も、それを踏襲し「72ポイント=1インチ」と言う基準にしました。

では、実際のディスプレイで考えてみます。ディスプレイは「ドット」がたくさん並んで構成されています。そのドットは物理的なものなので、大きくしたり小さくしたりは出来ません。
上記の「96ドット=論理インチ」を表現してみると、図4-15の左側のようになります。96ドットで1論理インチであり、また文字の大きさは「72ポイントで1インチ」なので、「72ポイント=96ドット(ピクセル)」となります。
ディスプレイ拡大率とDPI値
図4-15

しかしここで困ったことが起こります。技術が進み、ドットの大きさを小さくした「高解像度のディスプレイ」が出てくると、ドットの密度は高くなるので画質は綺麗になりますが、逆に「文字がどんどん小さく」なってしまうのです。
これでは困るので「1論理インチ当たりのドット数を上げる」機能が必要になってきます。これが「ディスプレイ拡大率」です。
図4-15の右側が例えばディスプレイ拡大率を150%にした時で、1論理インチ当たりのドット数は144(96dpi × 1.5 = 144dpi)になり、表示される文字も、100%拡大率の時と比べて1.5倍になる ことになります。

ということで「ポイントをピクセルに変換するには『0.75で割る』」というのは「ディスプレイ拡大率が100%の時」のことで、ディスプレイ拡大率が異なる時には「0.75で割り、ディスプレイ拡大率を掛ける」必要があります。なお、直接的に「DPI値/72 を乗ずる」でも良いです。
今回Module2の関数LogicalPixcelは、ディスプレイ拡大率を含んだ「DPI値」を取得するプロシージャです。

図形の位置が把握できたら、次に50行目の「GetCursorPos p」で「この時点でのマウス位置」を取得し、変数pに代入します。変数pはPOINTAPIの構造ですので、X方向の位置は「p.X」、Y方向の位置は「p.Y」で取得できることになります。単位はピクセルです。

ここまでで「図形の位置」と「マウスの位置」がピクセル単位で取得できましたので、重なっているか否かを52~53行目のIf文で確認します。If文は「If (SPleft <= p.X And SPright >= p.X And SPtop <= p.Y And SPbottom >= p.Y) Or (UFleft <= p.X And UFright >= p.X And UFtop <= p.Y And UFbottom >= p.Y) Then」となっており、2つの条件式がOrで繋がっています。

1つ目の条件式は「SPleft <= p.X And SPright >= p.X And SPtop <= p.Y And SPbottom >= p.Y」です。これを図で表せば図4-16のようになり、4つの不等号が全て成立すれば「図形とマウスが重なっている」ことになります。
図形とマウスの位置関係
図4-16

2つ目の条件式は「UFleft <= p.X And UFright >= p.X And UFtop <= p.Y And UFbottom >= p.Y」です。この条件式の意味は、図4-17のように「起動したダイアログとマウスが重なっているか」を確認しています。
ここで使っている「UFleft、UFright、UFtop、UFbottom」はUserForm2の位置を表すもので、UserForm2が起動した後のLayoutイベント(図7-5)で計算されるものです。
起動ダイアログとマウスの位置関係
図4-17

なお、ダイアログが起動する前(及び、ダイアログが消えた後)の状態では、ダイアログの位置を表す変数「UFleft・UFright・UFtop・UFbottom」は「全て初期値のゼロ」になってます。ですので、
 ・図形にマウスを近づけ、重なった時に反応しているのは1つ目の条件式で位置確認
 ・起動したダイアログの位置を変数「UFleft・UFright・UFtop・UFbottom」に代入
 ・ダイアログが表示された後は、1つ目と2つ目の条件式で位置確認
という順序で動いていることになります。

ここで注意しなければいけないのは、ダイアログを表示させる位置です。
最初に反応するのは「マウスが図形に重なった時」ですから、もし図4-18のように図形と離れた場所にダイアログを表示させた場合、「ダイアログを操作しようとしてマウスをダイアログの方に動かす」と、結果的に1つ目2つ目のどちらの条件式も満たさない場所をマウスが通過してしまいIF文が成立せず、ダイアログが消えてしまうことになります。
図形と起動ダイアログの位置関係
図4-18

ですので「ダイアログの表示位置は図形と重なっていること」が重要で、図7-2でUserForm2を表示させる際は「図形の位置を考えてダイアログを表示」させることが必要になります。

52~53行目のIf文で「図形とマウスが重なっている」または「起動したダイアログとマウスが重なっている」時に、54~55行目を実行します。

54行目の「LK = True」はフラグLKをTrueにします。内側Do~Loop(39~62行目)の繰り返し条件条件は「While LK = True And SystemStop = False」ですので、LK = True になることで(SystemStopフラグは、システムを中断しない限りFalseのため)内側Do~Loopを抜け出せず、同じ図形だけをチェックするようになります。図3-1で言うと左側のDo~Loopから右側のDo~Loopに移動することになります。

55行目の「Call UserForm2.UFstart(s)」では、代替テキストを表示編集するダイアログ(UserForm2)を起動します。但し「UserForm2.Show 0」のように直接起動するのではなく、UserForm2のフォームモジュール上にある「UFstart」プロシージャを呼び出す形にしています。理由は「ダイアログの表示位置を図形と重なった位置にする」ことを目的に、マウスと重なった図形を表す「s」を引数としてフォーム側に渡すためです。フォーム側では受け取った図形「s」の位置を元にして、フォームを表示する位置を決めています。

内側Do~Loopは、常にマウスの位置と図形・ダイアログの位置を確認しています。マウスと重なっている間は、常に54~55行目を実行し続けることになります。54行目の「LK = True」は、フラグLKにTrue値を入れ続けていますし、55行目の「Call UserForm2.UFstart(s)」も「同じフォームを何度も起動させよう」としています。
フラグLKに同じ値を入れ続けるのは問題無い(これも無駄と言えば無駄ですが)のですが、フォームを起動させ続けてしまうと「TextBoxの編集が出来ない」問題が発生しますので、フォーム側で「フォームが起動している間は、無視する」処理(図7-2の127行目)をしています。

内側Do~Loopが回り続けている途中で、もしマウスの位置が図形とダイアログの両方を外れた時には、57~58行目を実行することになります。まず57行目の「LK = False」では、内側Do~Loopを抜け出すようにフラグLKにFalse値を入れています。このコードにより、処理は内側Do~Loopから外側Do~Loopに移動(図3-1で言うと、左側のDo~Loopへ移動)することになります。

また58行目の「If UserForm2.Visible = True Then Unload UserForm2」で、表示していたダイアログを消去します。
ここで「If UserForm2.Visible = True Then」というIf文を付けていることについて説明します。
まず、IF文を付けていない場合は、マウスが図形に重なっていない状態の時には連続してUserForm2をUnloadしてしまいます。起動していないフォームをUnloadしても外観上は何も起きないのですが、Unloadするたびに「起動していないUserForm2の『UserForm_Terminate』イベントを実行」してしまいます。
今回、UserForm2の「UserForm_Terminate」イベントプロシージャ(図7-5)では、フォーム位置を表す変数を初期化(=フォームを消えた事にする)する作業しかしていないので「外観は何も起きない」ことになるのですが、無駄な動作のため「If UserForm2.Visible = True Then」を付けて、必要な時だけUnloadさせています。

次に、IF文を付けた時の弊害です。「If UserForm2.Visible = True Then」の部分で、UserForm2が起動しているか否かを確認しています。確認する際には「UserForm2」のInitializeイベント・Layoutイベントを呼び出します。但しDo~Loopは何回も回りますが、マウスが図形に重ならない内は「1回だけ呼び出せば、起動していない事を記憶している」ようです。なお、今回システムではInitializeは無いのでLayoutイベントだけが1回実行されます。
しかし、Layoutイベントを未起動の状態で実行されると、実際にはダイアログが無いのに「ダイアログが画面左上角に存在するかのような値が発生」してしまいます(詳細は、図7-5で説明します)。

ですので今回システムでは、58行目に「If UserForm2.Visible = True Then」を付けてTerminateイベントを連続実行しないようにすると共に、UserForm2のLayoutイベントにも「未起動時は無視する(図7-5の154行目)」コードを入れることで、無駄と誤作動の両方を抑えています。

66行目の「Loop While SystemStop = False」では、外側のDo~Loopの継続条件を「フラグSystemStop=False」としています。ON-OFFスイッチ(UserForm1)で停止させた時(フラグSystemStop=True)に、外側Do~Loopを抜け出し終了します。

5.標準モジュール2(Module2)

図4-8の34行目から呼び出される関数「LogicalPixcel」に関するコードだけをModule2にまとめています。
このLogicalPixcel関数は「ディスプレイ拡大率込みのdpi値」を戻します。
尚、この関数については他サイトでも色々取り上げられていますので、そちらも参照して頂ければと思います。

5-1.定数宣言とAPI宣言

まず、図5-1では、定数宣言とDeclare文でWindowsAPIの宣言をしています。
  1. '========== ⇩(4) DPI+ディスプレイ拡大率取得に使用する宣言 ============
  2. Private Const LOGPIXELSX = 88     '←画面水平方向のピクセル数(論理インチ当たり)
  3. Private Const LOGPIXELSY = 90     '←画面垂直方向のピクセル数(論理インチ当たり)
  4. Private Declare Function GetDesktopWindow Lib "user32" () As Long
  5. Private Declare Function GetDC Lib "user32" (ByVal hWnd As Long) As Long
  6. Private Declare Function GetDeviceCaps Lib "gdi32" (ByVal hdc As Long, ByVal nIndex As Long) As Long
  7. Private Declare Function ReleaseDC Lib "user32" (ByVal hWnd As Long, ByVal hdc As Long) As Long
図5-1

71~72行目の定数宣言は、何の固有情報を取得するかの設定です。今回は「dpi(dots per inch:1論理インチの幅の中にどれだけのドットを表現できるか)」を求めたいので71行目のLOGPIXELSX(値88)または72行目のLOGPIXELSY(値90)を使います。
なお、例えば定数LOGPIXELSXはExcelの定数では無いので、Const LOGPIXELSX = ・・・と定数宣言する必要があります。

ちなみに得られる固有情報は多くあるようで、その一部を図5-2で紹介します。
定数内容
HORZSIZE4物理画面の幅(単位mm)
VERTSIZE6物理画面の高さ(単位mm)
HORZRES8画面の幅(単位ピクセル)
VERTRES10画面の高さ(単位ピクセル)
BITSPIXEL12プレーン毎のピクセル当たりのカラービットの数
PLANES14カラープレーンの数
LOGPIXELSX88画面水平方向のピクセル数(論理インチ当たり)
LOGPIXELSY90画面水直方向のピクセル数(論理インチ当たり)
図5-2

74~77行目は、Declare文でWindowsAPIの宣言をしています。
74行目の「GetDesktopWindow」は、デスクトップのウィンドウハンドルを取得するものです。
75行目の「GetDC」は、デバイスコンテキストを取得するものです。
76行目の「GetDeviceCaps」は、デバイス固有情報を取得するものです。
77行目の「ReleaseDC」は、デバイスコンテキストを開放するものです。

5-2.dpiを取得する関数

dpiを取得する関数本体が図5-3です。
  1. '========== ⇩(5) dpiを取得する関数 ============
  2. Public Function LogicalPixcel() As Long
  3.  Dim hWndDesk As Long     '←デスクトップのウィンドウハンドル
  4.  Dim hDCDesk As Long     '←デバイスコンテキスト
  5.  hWndDesk = GetDesktopWindow()
  6.  hDCDesk = GetDC(hWndDesk)
  7.  LogicalPixcel = GetDeviceCaps(hDCDesk, LOGPIXELSX)
  8.  Call ReleaseDC(hWndDesk, hDCDesk)
  9. End Function
図5-3

84行目の「hWndDesk = GetDesktopWindow()」で、デスクトップのウィンドウハンドルを取得します。
85行目の「hDCDesk = GetDC(hWndDesk)」で、デスクトップのデバイスコンテキストを取得します。

87行目の「LogicalPixcel = GetDeviceCaps(hDCDesk, LOGPIXELSX)」では、デスクトップの「画面水平方向のピクセル数」を取得し、関数LogicalPixcelの戻り値に設定します。
なお、ここでは「画面水平方向(LOGPIXELSX)」を使用していますが、「ディスプレイ拡大率は、水平方向と垂直方向で拡大率が同じ」であるため、「画面垂直方向(LOGPIXELSY)」を指定しても同じ結果が得られます。

この「画面水平方向のピクセル数(LOGPIXELSX)」には、図5-2で「論理インチ当たり」と説明がついているように、ディスプレイ拡大率が100%の時の1インチと、例えば125%の時の1インチでは「1インチのサイズが異なり」、インチ当たりに含まれるピクセル数(dpi)が「1.25倍異なる」ことを利用して「ディスプレイ拡大率を含めたdpi値」を取得しています。
単純に考えれば、図5-4のような感じでしょうか。
ディスプレイ上の1インチ
図5-4

最後に89行目の「Call ReleaseDC(hWndDesk, hDCDesk)」で、デスクトップのデバイスコンテキストを解放し、他のアプリケーションから使用できるようにしています。

6.ユーザーフォーム1(UserForm1)

6-1.フォーム上のコントロールの配置

システムのON-OFFを行うフォーム1(UserForm1)上のコントロール類の配置は、図6-1のようにしました。
フォーム1の配置
図6-1

ToggleButtonを1個、CommandButtonを1個 並べて配置しています。ToggleButtonはValue値がTrueかFalseかでボタン形状が変わるため、今回のON-OFFを表すには適していると考えます。ボタン上の文字列はマクロ側から制御しています。
CommandButtonは、起動したこのフォームを終了させるためのボタンです。表面文字列は、配置時に手動でプロパティに書き込んでいます。

6-2.フォームモジュール

6-2-1.フォーム起動時設定

フォーム起動時に実行されるのが、図6-2のInitializeイベントプロシージャです。
  1. '========== ⇩(6) フォーム起動時設定 ============
  2. Private Sub UserForm_Initialize()
  3.  Me.Caption = "図形コメント"
  4.  Call ToggleButton1_Click
  5. End Sub
図6-2

94行目の「Me.Caption = "図形コメント"」では、ダイアログのタイトル文字列を書き込んでいます。
95行目の「Call ToggleButton1_Click」は、起動時に図6-3の「ToggleButton1_Clickプロシージャ」を1回呼び出すことで、ToggleButton表面文字列に「停止中」を書き込んでいます。
これはToggleButtonのValue値が既定ではFalseであることを利用し、「Me.ToggleButton1.Caption = "停止中"」という同じコードを重複しないようにしています。

一方、図6-3を呼び出すことで「システム停止フラグSystemStopにTrueを設定」するコードも同時に実行してしまいますが、この段階ではまだ「Do~LoopのSPsearchプロシージャ(図4-8)」を呼び出していませんので、無駄なコードを実行したという事になります。

6-2-2.トグルボタン操作時

トグルボタンをクリックすると、クリックする度に「ボタンが凹状態(Value=True)」と「ボタンが凸状態(Value=False)」を繰り返します。その時に発生するClickイベントプロシージャが図6-3です。
  1. '========== ⇩(7) トグルボタン操作時 ============
  2. Private Sub ToggleButton1_Click()
  3.  If Me.ToggleButton1.Value = False Then
  4.   Me.ToggleButton1.Caption = "停止中"
  5.   SystemStop = True
  6.  Else
  7.   Me.ToggleButton1.Caption = "起動中"
  8.   SystemStop = False
  9.   Call SPsearch
  10.  End If
  11. End Sub
図6-3

トグルボタンの形状が変わった後(=Value値が変更された後)にClickイベントが実行されますので、凹状態→凸状態に変化した後で102~103行目が実行され、逆に凸状態→凹状態に変化した後で105~107行目が実行されることになります。

先に「凸状態→凹状態(起動中)」にした時を説明します。
105行目の「Me.ToggleButton1.Caption = "起動中"」では、トグルボタンの表面文字列を「起動中」に書き換えます。
106行目の「SystemStop = False」では、「システム停止フラグSystemStop」にFalseを設定することで、SPsearchプロシージャ(図4-8)のDo~Loopを止めないようにします。
その上で107行目の「Call SPsearch」でSPsearchプロシージャを呼び出すことで、図形とマウスの位置を確認するDo~Loopが回り始めます。

次に「凹状態→凸状態(停止中)」にした時です。
102行目の「Me.ToggleButton1.Caption = "停止中"」ではトグルボタンの表面文字列を「停止中」に書き換えます。
次に103行目の「SystemStop = True」で、「システム停止フラグSystemStop」にTrueを設定します。トグルボタンを「凹状態→凸状態」にした時点ではSPsearchプロシージャのDo~Loopは回っていますが、このシステム停止フラグSystemStopがTrueになれば図4-8の62行目・66行目の脱出条件が成立し、Do~Loopを抜け出し「SPsearchプロシージャが終了する」ことで、システムを停止させています。

6-2-3.終了ボタン操作時

終了ボタンをクリックした時には「CommandButton1_Click」イベントが発生します。また、フォームを閉じる(ダイアログの右上×印をクリック)時には「UserForm_Terminate」が発生します。
  1. '========== ⇩(8) 終了ボタン押下時 ============
  2. Private Sub CommandButton1_Click()
  3.  End
  4. End Sub
  5. '========== ⇩(9) フォーム削除時 ============
  6. Private Sub UserForm_Terminate()
  7.  End
  8. End Sub
図6-4

終了ボタンをクリックするのも、ダイアログ右上×印をクリックするのも、ユーザーが「システムを終了したい」時ですから、114行目・119行目の「Endステートメント」で「マクロを強制的に中止」させています。
なお、114行目を「Unload Me」とすることでも同じ結果が得られます。これはダイアログを閉じる時には「Terminate」イベントが発生することから、最終的に119行目の「Endステートメント」が実行されるためです。

また、「Endステートメント」では無く「SystemStop = True」と言うコードを実行する方法を考える方も居ると思います。しかし、UserForm2が表示されている時に「SystemStop = True」をすると、内側のDo~Loopは脱出しても、まだFor Eachでのシート内図形検索が終了していない場合には「UserForm2が残留」してしまう場合があります(例:Focusしていた図形とUserForm1、それと他の図形が全て重なっていた場合など)。

その意味では、図6-3の103行目の「SystemStop = True」も不十分でUserForm2が残留する可能性がありますが、この場合は「システムを中断しているだけ」であり「次に実行させた時にはUserForm2は消える」ので、許容範囲と考えています。
(また、103行目で「End」ステートメントを使うとUserForm1も消えてしまいますので、「停止」という意味とはズレてしまう気がします。)

7.ユーザーフォーム2(UserForm2)

7-1.フォーム上のコントロールの配置

図形とマウス位置が重なった時に表示されるのが、「図形の代替テキストを表示・編集」するフォーム2(UserForm1)です。そのフォーム上のコントロール類の配置は、図7-1のようにしました。
フォーム2の配置
図7-1

TextBoxを1個、CommadnButtonを2個配置しています。TextBoxは図形の代替テキストを表示させ、且つ編集もさせる場所です。CommandButtonの1つ目は「編集したものを保存」するためのボタンです。
もう1つのCommandButtonは「編集をキャンセルさせる」役目と「表示されたダイアログを消去する」役目を持たせたつもりですが、マウスを図形・ダイアログから外してしまえば両方の役目は達せられるので、あまり意味の無いボタンかもしれません。

2つのCommandButtonの表面の文字列は、配置時にプロパティを変更して記述してます。

7-2.フォームモジュール

7-2-1.標準モジュールからの呼び出されるプロシージャ

図4-8の55行目から呼び出される「UFstart」が図7-2です。引数として「マウスの位置と重なった図形」である「s(Shapeオブジェクト)」を受け取ります。
  1. '========== ⇩(10) モジュール内変数の宣言 ============
  2. Dim Sp As Shape     '←マウスの位置と重なった図形オブジェクト
  3. '========== ⇩(11) フォーム起動プロシージャ ============
  4. Sub UFstart(s As Shape)
  5.  If Me.Visible = True Then Exit Sub
  6.  Set Sp = s
  7.  Me.TextBox1.Value = Sp.AlternativeText
  8.  Me.StartUpPosition = 0
  9.  Me.Left = ActiveWindow.PointsToScreenPixelsX(0) * 0.75 / WinScreenRatio _
  10.       + Sp.Left * (ActiveWindow.Zoom / 100)
  11.  Me.Top = ActiveWindow.PointsToScreenPixelsY(0) * 0.75 / WinScreenRatio _
  12.       + Sp.Top * (ActiveWindow.Zoom / 100)
  13.  Me.Show 0
  14. End Sub
図7-2

123行目の「Dim Sp As Shape」は、モジュールレベルの変数宣言です。呼び出された「UFstartプロシージャの引数」として受け取った図形オブジェクト「s」を「UserForm2モジュール内で『Sp』として共有」するためのものです。
そのために129行目の「Set Sp = s」で、引数として得た「s」を引数「Sp」に代入しています。

それより1つ前の127行目の「If Me.Visible = True Then Exit Sub」は、「ダイアログが立ち上がっている時は、スルーさせる」という意味です。
図4-8の55行目の「Call UserForm2.UFstart(s)」は、マウスと図形が重なっている間ずっと実行され、その都度UFstartプロシージャが呼び出されるわけですが、127行目のコードにより「フォームを起動させ続け無い」ようにし、「TextBoxの編集が出来る」ようにしています。

132行目の「Me.StartUpPosition = 0」は、フォームの表示位置を指定するものです。図4-7の表で示した通り、「ゼロ」の指定は「表示位置は手動(マクロ)で設定」するという意味になります。設定する値は、フォームの位置「Left値」「Top値」ですので、図4-3で示した通り、ポイント単位で且つスクリーン座標で指定することになります。

まず、どこにダイアログをどこに表示すれば良いか ですが、図4-18のように「図形とダイアログが離れている」と、「図形からダイアログにマウスが移動している最中に、図形からもダイアログからも外れてしまう」ことになります。ですので図形に対してダイアログは重ねる必要がありますが、その重ね方は図7-3の4通りくらいだと思います。
フォームと図形の重ね方
図7-3

図形の左側や上側にダイアログを置くバージョンが無いのは、指示するポイント数がマイナスになった時の計算が面倒だからです。(マイナス値を設定してもエラーは出ませんが、画面から外れてしまうので、見えなくなったり操作できなくなったりします。)
図形が小さな場合も考えられますので、今回は①の「図形の左上角とフォームの左上角を合わせる(=完全に重ねる)」方法にしました。

図形の位置は、図4-8の41~42行目(Left方向)、45~46行目(Top方向)で計算しています。但しこの時の単位は「ピクセル」にしていますので、フォーム位置用の「ポイント」に変換する必要があります。
よりみち」でも説明したように、ピクセルをポイントに変換するには「0.75を掛け、ディスプレイ拡大率で割る(ポイント→ピクセル変換の時に対して逆)」ことで値が得られます。

まず図形のLeft方向の式は「SPleft = ActiveWindow.PointsToScreenPixelsX(0) + (s.Left / 0.75) * (ActiveWindow.Zoom / 100) * WinScreenRatio」です。これに「0.75 / WinScreenRatio」を掛ければ良いわけです。
すると、133~134行目のように、
「ActiveWindow.PointsToScreenPixelsX(0) * 0.75 / WinScreenRatio + Sp.Left * (ActiveWindow.Zoom / 100)」
となり、これをUserForm2のLeft値として指定します。

また図形のTop方向の式「SPtop = ActiveWindow.PointsToScreenPixelsY(0) + (s.Top / 0.75) * (ActiveWindow.Zoom / 100) * WinScreenRatio」に対しても「0.75 / WinScreenRatio」を掛ければ、135~136行目のように、
「ActiveWindow.PointsToScreenPixelsY(0) * 0.75 / WinScreenRatio + Sp.Top * (ActiveWindow.Zoom / 100)」
となり、これをUserForm2のTop値として指定します。

似たような計算式を書きたく無いのであれば、図4-8の変数SPleft・変数SPtopを引数や共有変数にしておくなどの方法で、UserForm2へ渡し、その変数を加工する という手法も考えられます。

フォームを表示する位置が決まったら、138行目の「Me.Show 0」で、モードレスで表示します。

7-2-2.ボタンの操作

UserForm2のダイアログ上には、「保存」と「キャンセル」のボタンを配置しています。
「保存」ボタンの役目は、「編集されたTextBoxの文字列を、図形の代替テキストに保存」するものです。もし編集していなくても、保存ボタンをクリックすれば、代替テキストが上書きされます。
「キャンセル」ボタンは、「編集を保存せずに閉じる」役目と、「ダイアログを消去する」役目がありますが、マウスを図形・ダイアログから外せば自動的に「(もしTextBoxが編集されていても保存せずに)閉じる」ので、あまり意味はないかもしれません。
  1. '========== ⇩(12) 保存ボタンの処理 ============
  2. Private Sub CommandButton1_Click()
  3.  Sp.AlternativeText = Me.TextBox1.Value
  4.  Unload Me
  5. End Sub
  6. '========== ⇩(13) キャンセルボタンの処理 ============
  7. Private Sub CommandButton2_Click()
  8.  Unload Me
  9. End Sub
図7-4

「保存ボタン」をクリックした時に実行されるのが142~145行目のClickイベントです。
143行目の「Sp.AlternativeText = Me.TextBox1.Value」で、TextBoxの内容をFocusしている図形の代替テキスト(AlternativeText)に代入します。代入した後は144行目の「Unload Me」で、ダイアログを閉じます。
編集した内容が正しく保存されているかは、図形から一度マウスを外し、再度Focusを当てることで確認できます。なお、ボタンをクリックしたマウスの位置が「Focusしている図形に重なってる」場合は、一旦ダイアログが消えてから再度ダイアログが自動的に起動します。

「キャンセルボタン」をクリックした時は、148~150行目のClickイベントが実行されます。
149行目の「Unload Me」で、単純にダイアログを閉じます。但し「保存ボタン」の時と同様に、ボタンをクリックしたマウスの位置が「Focusしている図形に重なってる」場合は、一旦ダイアログが消えてから再度ダイアログが自動的に起動します。

ちなみに、ダイアログの右上×印をクリックすると「Unload Me」を実行したのと同じことになりますので、今回特にイベントを使っての処理はしていません。

7-2-3.フォーム位置の把握

フォームが起動する時には、「Initialize」→「Layout」→「Activate」という順序でイベントが発生し、ダイアログとして表示されます。また、ダイアログを移動すると「Layout」イベントが発生します。
ダイアログを閉じる時には、まず「QueryClose」イベントが発生し、そのあとダイアログが消えてから「Terminate」イベントが発生します。

ダイアログが起動している間は「ダイアログの位置・サイズの情報」を図4-8の内側Do~LoopのIF文に渡して、そのダイアログの上でマウス操作が出来るようにする必要があります。起動直後及び移動時の両方でLayoutイベントが発生しますので、「ダイアログ表示中の位置・サイズ情報」はLayoutイベント(図7-5の153~160行目)で作成することにしました。

また、ダイアログが消えた後は、図4-8の外側Do~LoopのIF文で「図形だけに反応」させる必要があるので、ダイアログ位置・サイズ情報は「全て初期値(全てゼロ)」にする必要があります。この処理は、「QueryClose」「Terminate」どちらのイベントでもOKなのですが、「ダイアログが消去された後」の処理のため「Terminate」イベント(図7-5の163~168行目)の方が正しいと考えました。
  1. '========== ⇩(14) フォーム位置の把握 ============
  2. Private Sub UserForm_Layout()
  3.  If Me.Visible = False Then Exit Sub
  4.  UFleft = Me.Left / 0.75 * WinScreenRatio
  5.  UFtop = Me.Top / 0.75 * WinScreenRatio
  6.  UFright = (Me.Left + Me.Width) / 0.75 * WinScreenRatio
  7.  UFbottom = (Me.Top + Me.Height) / 0.75 * WinScreenRatio
  8. End Sub
  9. '========== ⇩(15) フォーム位置の消去============
  10. Private Sub UserForm_Terminate()
  11.  UFleft = 0
  12.  UFtop = 0
  13.  UFright = 0
  14.  UFbottom = 0
  15. End Sub
図7-5

先に156~159行目を説明します。156~159行目は、起動しているダイアログの位置を変数に代入して図4-8のIF文に渡しています。
ここでUserForm2の位置「Me.Leftなど」の単位は「ポイント」ですが、図4-8のIF文は「ピクセル」で統一しているため、ポイント単位の位置をピクセルに変換する必要があります。
ポイントをピクセルに変換するには、「よりみち」でも説明したように、「0.75で割り、ディスプレイ拡大率を掛ける」ことで値が得られますので、例えばLeft方向は156行目のように「UFleft = Me.Left / 0.75 * WinScreenRatio」とします。
他のTop・Right・Bottomについても同じ方法で式を立てます。

戻って154行目の「If Me.Visible = False Then Exit Sub」は、「ダイアログが起動していない時は、スルーする」という意味です。一見、意味の無いコードに思えますが、意外な現象でこのLayoutイベントが発生してしまうことが分かりました。

UserForm1を起動し、トグルボタンをクリックして「起動中」にすると、図4-8の外側Do~Loopが回り始めます。起動直後はマウスは図形から外れているのが普通なので、図4-8の52~53行目のIF文は成立せず58行目の「If UserForm2.Visible = True Then Unload UserForm2」を実行します。
この段階ではUserForm2は起動してませんので、素直に次の行にコードは移るはずと思っていたのですが、「UserForm2が起動しているか否か」を確認するためにUserForm2の「Initializeイベント」→「Layoutイベント」を動かしているようです。
今回は「Initialize」が無いので、「Layoutイベント」だけを動かすのですが、もし154行目の「If Me.Visible = False Then Exit Sub」が無いと、156~159行目を実行することになります。

156~157行目のUFLeft・UFTopは、UserForm2が起動していないので両方とも値がゼロになるのですが、フォームの幅(Width)と高さ(Height)は実在しますので、図7-6の②ように158~159行目のUFRight・UFBottomには値が入ってしまいます。
フォームが起動中か否かの確認
図7-6

UFRight・UFBottomに値が入ってしまうと、図4-8の52~53行目のIF文は「UserForm2が画面左上角に存在しているかのような状態」になってしまいます(図7-7)。
フォームが画面左上角に存在しているかのような状態
図7-7

その時マウスは、まだ画面左上に存在しますので、52~53行目のIF文は成立してしまうため、図4-8の55行目の「Call UserForm2.UFstart(s)」が実行され、「図形とマウスが重なっていない」のに図7-8のように「ダイアログが表示」されてしまいます。
マウスが図形に重なっていないのに、フォームが起動
図7-8

しかしUserForm2が起動してみると、その位置は図形の場所にある(画面左上角では無い)ので「マウスが図形・ダイアログから外れている」と判断され、ダイアログは削除されます。

このように、図7-6の「②→①→③→②→①→③→・・・」と繰り返してしまうことで、マウスが左上角にある限りは「各図形のダイアログが現れたり消えたり」チラチラする現象が発生します。
この対策が154行目の「If Me.Visible = False Then Exit Sub」で、悪循環を最初で断ち切ることでチラチラを防止できます。

フォームが消えた時には、図4-8のIF文からダイアログ分を差し引くために、164~167行目のようにLeft・Top・Right・Bottomの値にゼロを代入します。

8.アドインとしてExcelにマクロを登録

このマクロをExcelのアドインに登録することで、どのブックでも「図形のコメント閲覧編集」を使用することが出来ます。アドイン方法については「年賀状リスト等の宛名検索と追記 アドイン登録」を参照下さい。
またアドイン登録した際の実行マクロは、図4-6の「SystemStartプロシージャ」にして下さい。

9.最後に

今回システムはコードの行数はそれほど多く無い割りに、シート上の図形の位置・ディスプレイ上のマウスの位置とフォームの位置を同じ指標で統一して計算させるのは結構面倒でした。
また、今までの回ではあまり気にしていなかった「ディスプレイの倍率」について今回考慮したのは、作っている途中のExcelファイルを自分の周りのいくつかのPCで試してみたところ、思い通りに動かないPCがあることに気が付いたからです。ご想像の通り「ディスプレイ拡大率が100%以外」に設定されていたからです。
ディスプレイ拡大率の存在は知っていたのですが「普通のPCは100%だろう」と高をくくっていたのが間違いの元でした。

また今回は、Excelの便利な機能である「ウィンドウ枠の固定」や「ウィンドウの分割」には対応できていません。これに対応するには、あまりに面倒が多い気がしますし、また他サイトを見てもあまり扱っていないようです。その内トライしたい課題ではあります。


シート上の図形の代替テキストを閲覧・編集(it-060.xlsm)

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