2023/05/28

ドキュメント座標~スクリーン座標間の換算




Excelを扱う時には、Excelシート上の「ドキュメント座標」だけで無く、パソコンの画面上である「スクリーン座標」を意識しなければならない場合があります。また長さの単位にも「ポイント(以下ptと略す場合あり)」と「ピクセル(以下pxと略す場合あり)」の2つがあり、合計4種類の間を変換する時には、単位換算のみでは無く、Excelシートの「表示倍率」やパソコンの「ディスプレイ拡大率」が関わってきます。
更にExcelには「ウィンドウ枠の固定」や「画面分割」という機能もあるため、見た目の距離を変換するのが大変です。
今回は、これらの関係について整理していきます。

1.スクリーン座標とドキュメント座標の関係

まず「スクリーン座標(画面座標、ディスプレイ座標 とも呼ばれる)」は、文字通りパソコン画面上での座標です。その原点は、図01の左側のようにスクリーンの「左上角」になります。
パソコンでExcelを開くと、このスクリーン上に「Excelが浮いている」ような状態で表示されます。この表示されたExcelのシート上が「ドキュメント座標」になり、その原点はシートの左上角(A1セルの左上)になります。
スクリーン座標とドキュメント座標
図01


Excelのシートである「ドキュメント座標」の上に存在するものは、Excelのセルやシートに貼り付けた図形などです。これらはExcelのシート以外の場所に動かすことはできません(図01の右側)。一方ユーザーフォームやメッセージボックスはExcelシートから離れた場所にも動かすことが可能ですし、Excelの本体もスクリーン上のどこへでも移動可能です。

各座標上の位置は、原点からのX・Y方向の距離のセットで表します。図02のように各座標とも「左右がX軸で、右方向がプラス」「上下がY軸で、下方向がプラス」です。Excelのオブジェクトの位置を表すには、X方向はLeftプロパティを、Y方向はTopプロパティが使われます。
スクリーン座標とドキュメント座標の原点の違い
図02


このスクリーン座標とドキュメント座標の違いは、位置の基準である「原点位置」が違うというより、図01の右側で「図形はシートの外には出られず、ユーザーフォームは出られる」ことからも、住んでいる世界が異なると考えた方が分かり易いかもしれません。
例えとして、スクリーン座標を「海」に、ドキュメント座標をその海に浮かんでいる「大型船」に置き換えてみます。
船自体の位置は、緯度経度という絶対値である「海の基準」で、浮いている場所が特定できます。しかし船上に存在するもの、例えば船長室とか展望デッキの位置には、船内の地図や番地を使います。もし船が海の上を移動しても、船上での相対的な位置は変わらないからです。
と言って船長室の位置を海基準の緯度経度で表す必要がある場合は、直接GPS計で測る以外にも、「船の位置(海基準)+船上での位置(船基準)」という計算で表すことも可能です。

この海(=スクリーン座標)の中での「船(=ドキュメント座標)の位置」は、図03のように「PointsToScreenPixelsX(X方向)」、「PointsToScreenPixelsY(Y方向)」というメソッドで取得できます。このメソッドの引数には、ドキュメント座標上の位置を指定するのですが、図03ではゼロを指定していますので「ドキュメント座標の原点」の位置が得られることになります。
ドキュメント座標の原点が取得できたら、後はExcelシート上での通常の位置を計算し、足し合わせれば「スクリーン座標から見た位置」が分かることになります。
スクリーン座標とドキュメント座標の重なり
図03


もちろん「PointsToScreenPixelsX メソッド」等の引数に、オブジェクトの位置を表す値(ゼロ以外)を指定することは可能ですが、注意点があります。
「PointsToScreenPixelsX 、Y」メソッドの親となるオブジェクトは2つ存在します。「Windowオブジェクト」と「Paneオブジェクト」です。例えばWindowオブジェクトを使うと「ActiveWindow.PointsToScreenPixelsX(引数値)」となりますし、Paneオブジェクトを使うと「ActiveWindow.Panes(1).PointsToScreenPixelsX(引数値)」などとなります。なおPaneオブジェクトは分割された画面の1つを指します(画面分割していない場合は、Panes(1) のみが存在します)。

画面分割をしていない状態で、引数にゼロを指定した時にはどちらのオブジェクトでも正しい値を示します。しかし引数にゼロ以外を指定すると、なぜか「Windowオブジェクトは異常値」を示します。
また画面分割をしている状態では「Windowオブジェクトは引数がゼロでもゼロ以外でも異常値」を示します。
Microsoftの説明には異常値が出るような事は書いていないので、Excelのバグのようです。その内に修正がされるかもしれませんが、少なくともそれまでは「ActiveWindow.Panes(1).PointsToScreenPixelsX(引数値)」と、Paneオブジェクトに対するメソッドとして使用する方が良いと思います。
寄り道
このWindowオブジェクトでの「異常値」ですが、他のサイトでも「変な値が出る」との報告があります。また見出しとして使っている幅や高さの「2倍だけズレている」との報告もあります。

図04は3行×2列のウィンドウ枠固定をしたワークシートですが、これを使って私も試してみました。
ActiveWindow.PointsToScreenPixelsX(0) / Y(0)での異常値
図04


結論的には「ウィンドウ枠固定やウィンドウ分割を行っていると、WindowオブジェクトのPointsToScreenPixelsX /Yメソッドでは、正しい値が出ない」事が確認できたのですが、どういう計算をしているか考えてみました。
私の推定では、ActiveWindowを「Panes(1)では無く、Panes(ActiveWindow.Panes.Count)と勘違い」して、タイトル部の幅・高さ分を引算しているようです。しかも行番号・列番号の幅・高さまで誤って引いてしまっているようです。
列番号の高さは「ほぼセル1個分」なので、タイトル行が1行であれば「ほぼ2倍のズレ」という他サイトの報告も頷けます。

今までこのサイトでも「PointsToScreenPixelsX(0) /Y(0)」は何度も出てきますが、恥ずかしながら全てWindowオブジェクト(≒ Panes(1)を入れていない)で使ってきました。私自身ほとんどウィンドウ枠固定やウィンドウ分割を使わないもので、エラーに気が付きませんでした。申し訳ありません。

2.ポイントとピクセルの関係

ポイントとピクセルの関係は、一般的には以下の式が使われます。
72ポイント=96ピクセル
「ピクセル → ポイント」換算時は、ピクセル値に「 72/96 (=0.75) 」を掛ける 
「ポイント → ピクセル」換算時は、ポイント値を「 72/96 (=0.75) 」で割る
この関係は、以下の関係から導かれます。
 1ポイント=1/72インチ
 1ピクセル=1/96インチ

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

次にピクセルとは「ディスプレイのドット」で、1インチ当たりのドット数を「DPI(Dot per Inch)」として表します。上の式は「1インチ当り 96ドット」という事を示していることになります。
この値を私のノートPCの画面寸法と解像度から検証したのが図05です。なんと「約148dpi」となります。
実DPIの確認
図05


実は、DPIのインチは実際の長さでは無く「論理インチ」で、「96ドットを論理インチ」とするという意味になります。
この背景には、世の中の様々なサイズ・性能のディスプレイに対応させることと併せて、手元の本(ポイント単位)よりもディスプレイが遠くにあるため、ポイントよりも1/3だけサイズを大きく(72→96)したという理由もあるようです。

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


ですので、図03で説明したような「船の位置(スクリーン座標)+船上での位置(ドキュメント座標)」を実際のExcelのコードで表すと以下の様になります。なおここでは「船上での位置=ワークシートのF6セル」とし、単位としてはピクセルにまとめています。
 スクリーン座標上のX軸(単位ピクセル) = ActiveWindow.Panes(1).PointsToScreenPixelsX(0) + Range("F6").Left / 0.75
 スクリーン座標上のY軸(単位ピクセル) = ActiveWindow.Panes(1).PointsToScreenPixelsY(0) + Range("F6").Top / 0.75

「PointsToScreenPixelsX / Y」メソッドは、ドキュメント座標原点位置をピクセル単位で戻してきます。そのドキュメント座標原点を基準としている「Range("F6").Left」等はポイント単位で位置を戻してきますので、「 72/96 (=0.75) 」で割れば良いことになります。

または以下のように、PointsToScreenPixelsX / Yの引数に直接ポイント値を入力してもOKです。この時、上記で説明した「Panes(1).」を必ず付けてPaneオブジェクトのPointsToScreenPixelsX / Y メソッドとすることが重要です。
 スクリーン座標上のX軸(単位ピクセル) = ActiveWindow.Panes(1).PointsToScreenPixelsX(Range("F6").Left)
 スクリーン座標上のY軸(単位ピクセル) = ActiveWindow.Panes(1).PointsToScreenPixelsY(Range("F6").Top)

なお図03では「スクリーン座標=単位ピクセル、ドキュメント座標=単位ポイント」のような書き方をしていますが、あまり正確な説明ではありません。例えばスクリーン座標で動くユーザーフォームの位置(Left、Topプロパティ)の単位はポイントです。他にも例外があるかは分かりませんが、単位は良く調べてから使用した方が良いと思います。

3.ディスプレイ拡大率

上記「1ピクセル=1/96論理インチ」と決まったのは1990年代でした。その後2000年代になり技術が進んで、ドットの大きさを小さくした「高解像度ディスプレイ」が出てきました。すると、ドットの密度は高くなるので画質は綺麗になりますが、逆に「文字が小さすぎて読みにくい」というデメリットも出てきます。
その解決策として「1論理インチ当たりのドット数を上げる」という機能が出てきました。これが「ディスプレイ拡大率」です。

ディスプレイ拡大率は、設定から「システム」→「ディスプレイ」→「拡大縮小とレイアウト」内の、「拡大/縮小」で設定します(図07)。Windowsのバージョンにより表現は色々ですが、「拡大」「縮小」の言葉が入っているはずです。
ディスプレイ拡大率の変更の仕方
図07


変更できるディスプレイ拡大率の範囲はディスプレイの解像度によって異なりますが、図07(私のノートPC)では「100%・125%・150%」の3種類から選択できます。
また、同じ画面で「ディスプレイ解像度」も変更可能ですが、これは複数のドットを連動させ疑似的な大きなドットを作る事で、画像を粗くして画面を拡大するものです。通常は一番上の「推奨(=物理的なドットを全て有効に使う)」を選択した方が良いと思います。

上記で説明してきた「1ピクセル=1/96論理インチ」という関係は、実は「ディスプレイ拡大率=100%」の時だけで、それ以外の時には図08のように異なるピクセル値となります。
ディスプレイ拡大率100%125%150%175%200%225%250%
ピクセル/論理インチ96120144168192216240
図08


例えばディスプレイ拡大率「100%」と「150%」を、図09のように画面を拡大して比較してみます。
ディスプレイ拡大率違いでの表示内容
図09


図09の左側は、今まで説明してきた「1ピクセル=1/96論理インチ」です。逆に言えば 96ピクセル/論理インチで、且つ その大きさが72ポイントになります。
その同じディスプレイで、ディスプレイ拡大率を150%(図09の右側)にすると、144ピクセル分が1論理インチとなりますので、自動的に文字サイズも大きく(図09の右側は、左側の1.5倍)なる事になります。

ディスプレイ拡大率を取得するには、Win32 APIの「GetDeviceCaps関数」を使います。GetDeviceCaps関数は「デバイス(ここではディスプレイ)情報」を取得する関数で、2つの引数を指定します。
 GetDeviceCaps(デバイスコンテキストのハンドル, 項目を示す値)
項目は図10のように複数あり、ディスプレイの様々な値を得ることが可能です。

図10の右列には、私のノートPCでの「ディスプレイ拡大率100%」の時の値と「150%」の値を並べていますが、値が異なるのは「88(LOGPIXELSX)」と「90(LOGPIXELSY)」のみです。X方向とY方向の拡大率は同じですので、今回は「88 又は 90」を使って「論理インチ当りのピクセル数」を取得しています。 取得するコードについては、図40を参照下さい。
定数項目例(私のノートPC)
100%150%
DRIVERVERSION0デバイスドライバのバージョン16384
TECHNOLOGY2デバイステクノロジー*1
HORZSIZE4物理画面の幅(単位mm)263
VERTSIZE6物理画面の高さ(単位mm)175
HORZRES8画面の幅(単位ピクセル)1536
VERTRES10画面の高さ(単位ピクセル)1024
BITSPIXEL12ピクセルあたりのカラービット数32
PLANES14カラープレーンの数1
NUMBRUSHES16デバイス固有のブラシの数-1
NUMPENS18デバイス固有のペンの数-1
NUMMARKERS20デバイス固有のマーカーの数0
NUMFONTS22デバイス固有のフォントの数0
NUMCOLORS24デバイスのカラーテーブルのエントリ数-1
PDEVICESIZE26(予約値)0
CURVECAPS28デバイスの曲線描画能力*511
LINECAPS30デバイスの直線描画能力*254
POLYGONALCAPS32デバイスの多角形描画能力*255
TEXTCAPS34デバイスのテキスト表示能力*30727
CLIPCAPS36デバイスのクリッピング能力1
RASTERCAPS38デバイスのラスタ能力*32409
ASPECTX40線の描画に使うデバイスピクセルの相対幅36
ASPECTY42線の描画に使うデバイスピクセルの相対高さ36
ASPECTXY44線の描画に使うデバイスピクセルの対角線の長さ51
SHADEBLENDCAPS45デバイスのシェードとブレンドの能力を示す値**0
LOGPIXELSX88論理インチ当たりの画面の水平方向のピクセル数96144
LOGPIXELSY90論理インチ当たりの画面の垂直方向のピクセル数96144
SIZEPALETTE104システムパレット内のエントリ数0
NUMRESERVED106システムパレット内の予約エントリ数20
COLORRES108デバイスの実際のカラー解像度を表す、ピクセル当たりのビット数24
PHYSICALWIDTH110物理的なページ全体の幅(デバイス単位)0
PHYSICALHEIGHT111物理的なページ全体の高さ(デバイス単位)0
PHYSICALOFFSETX112物理的なページの左辺から印刷可能領域の左辺までの距離(デバイス単位)0
PHYSICALOFFSETY113物理的なページの上辺から印刷可能領域の上辺までの距離(デバイス単位)0
SCALINGFACTORX114X軸のスケーリングファクター0
SCALINGFACTORY115Y軸のスケーリングファクター0
VREFRESH116現在のディスプレイ出力の垂直周波数 (Hz)60
DESKTOPVERTRES117仮想デスクトップの高さ(ピクセル単位)***1024
DESKTOPHORZRES118仮想デスクトップの幅(ピクセル単位)***1536
BLTALIGNMENT119デバイスに適した水平方向のアラインメント***1
*=対照表が存在  **=Windows 98/2000以降  ***=Windows NT/2000/XPのみ
図10


この「GetDeviceCaps関数」で得られた「論理インチ当りのピクセル数」を、拡大率100%時の「96」で割ってディスプレイ拡大率を計算します。
 ディスプレイ拡大率 = GetDeviceCaps関数で得られた論理インチ当りのピクセル数 ÷ 96

この「ディスプレイ拡大率」を使って、どのように補正していくかを考えるために、ディスプレイ拡大率を100%と150%にした画面を図11で比較してみました。
現象として、Windowsのアイコンを含め、Excel本体及びドキュメント座標に描かれた図形の大きさも1.5倍になっているのが確認できると思います。
ディスプレイ拡大率違いでのExcel
図11


また図11では、Excel本体の左上角の位置はほとんど移動していない事が分かります。ディスプレイ拡大率を大きくした際に、Excel本体の右側や下側がWindows画面の右端や下端にぶつかってしまうと、押し出されて画面の左上角の方向に移動してしまう様ですが、ぶつからない限りは「左上角位置を保つ」様です。

つまり単位をピクセルとして見た場合には、ドキュメント座標の原点は変わらず、ドキュメント座標上にある図形やセルの位置はディスプレイ拡大率の分だけ変わる事になります。
よって、Excel上の位置(pt)と画面ピクセルの換算式をディスプレイ拡大率で補正するには、以下の様になります。
 スクリーン座標上の位置(px) = ドキュメント座標原点(px) + ドキュメント座標上の位置(pt)/0.75 × ディスプレイ拡大率
この式中の ドキュメント座標原点(px)は、「ActiveWindow.Panes(1).PointsToScreenPixelsX(0)」等を指します。

実例として、Excel本体のディスプレイ上での左右位置は「Application.Left」としてポイント単位での取得ができますが、これをピクセルで表すには以下のようにします。
 Excel本体の左右方向の位置(ピクセル)= Application.Left / 0.75 * ディスプレイ拡大率
つまりスクリーン座標上のピクセル値が変わらないだけで、ディスプレイ拡大率が変更された事で「ポイント値/ピクセル値」は変わる為、Excel本体の位置を表すLeft値・Top値(ポイント単位)は「ディスプレイ拡大率の分だけ変わる」ことになります。

4.Excelの表示倍率

Excelにはシートを拡大/縮小する機能(表示倍率)があります。シート上の内容を同じ状態にしたまま、表示倍率を変更したのが図12です。
表示倍率違いでのExcel
図12


図12の各倍率のシート上には、100%の時に丁度100ポイント角になるような「緑色破線の四角枠」を乗せています。シートを拡大/縮小してもExcel本体の大きさは変わらないので、表示倍率を100% → 50%にすることで「ポイントの距離が半分」になることが分かります。つまり、以下の関係が成立する事になります。
 スクリーン座標上の位置(px) = ドキュメント座標原点(px) + ドキュメント座標上の位置(pt)/0.75 × ディスプレイ拡大率 × Excel表示倍率
この式中の ドキュメント座標原点(px)は、「ActiveWindow.Panes(1).PointsToScreenPixelsX(0)」等を指します。

なお「Excel表示倍率」は「ActiveWindow.Zoom」で得られますが、戻り値は「100%時=100」「150%時=150」などと「%単位」となりますので、数式内に入れ込む時には「ActiveWindow.Zoom / 100 」のようにする必要があります。

また、例えばExcelのドキュメント座標上に配置した図形の位置(単位:ポイント)は、Excelの表示倍率を変更してもポイント数は同じですが、Excelから起動したユーザーフォームはスクリーン座標上で動いているにも関わらず「Excel表示倍率が100%時のポイント数」で位置が決まるため、フォームの位置を考える場合は「Excel表示倍率も考慮」する必要が出てきます。

5.ウィンドウ枠の固定と画面分割

5ー1.特徴

ウィンドウ枠の固定または画面分割を設定すると、以下のような状態になります。
ウィンドウ枠の固定と画面分割の状態
図13


見た目は似ていますが、図13の左側の「ウィンドウ枠の固定」は、見出し行/見出し列を作るもので、固定後は見出し行位置・見出し列位置は変更できません。
可動領域は右下(上下のみに固定した場合は下側が可動領域、左右のみに固定した場合は右側が稼働領域)のみで、スクロールが可能です。また、図14の左側のように固定部分と可動部分でセルが重複表示されることはありません。

一方図13の右側の「ウィンドウの分割」は、1つのシートを4つの小さな窓から見るようなものです。
もちろんウィンドウ枠固定のように見出しとして使用することも可能ですが、4領域(上下方向分割・左右方向分割の場合は2領域)ともスクロールすることが出来ます。また、4つの窓から同じ場所を見ることも可能で、図14の右側のように表示するセル範囲が重複することもあります。
ウィンドウの分割は表示セルが重複することもある
図14


また「ウィンドウ枠の固定」なのか「ウィンドウの分割」なのかを判断するには、ActiveWindow.FreezePanesプロパティを調べます。Trueであれば「ウィンドウ枠の固定」であり、Falseであれば「ウィンドウの分割」です。なお固定も分割もしていない通常の状態はFalseです。

5ー2.領域の数

領域の数は見た目の通りで、分割をしていなければ1個、上下分割や左右分割であれば2個、上下+左右分割だと4個となります。この領域数は「ActiveWindow.Panes.Count」プロパティで得られます。
領域の数
図15


図15は「ウィンドウの分割」の例ですが、「ウィンドウ枠の固定」を上下・左右・上下+左右で行っても、同じく「ActiveWindow.Panes.Count」プロパティで領域数が得られます。

5ー3.各領域の指定方法

その分割・枠固定した各領域は、図16のように「ActiveWindow.Panes(i)」というオブジェクトで指定できます。引数のiは、場所を表します。
領域の指定の方法
図16


まずドキュメント座標原点に最も近い領域は「ActiveWindow.Panes(1)」となります。固定・分割していない状態ではActiveWindow.Panes(1)のみが存在する形になります。
上下左右の4領域の場合は、原点を含む左上領域を1とし、その右側が2、左下が3、右下(一番原点から遠い領域)が4となります。
上下2分割の場合は、原点を含む上側が1、下側が2。左右2分割の場合は原点を含む左側が1、右側が2となります。

まとめると原点に最も近い側、つまり「分割の影響を受けていない領域」がActiveWindow.Panes(1)で、「最も分割されている領域」がActiveWindow.Panes(ActiveWindow.Panes.Count) となります。

また各領域の「左上角のセル」は「ActiveWindow.Panes(i).VisibleRange.Cells(1)」で表す事が出来ます。「現在見えている領域の1番目のセル」という意味(Rangeオブジェクト)です。スクロールバーで見えている領域の範囲を変更すると、その左上角セルも変更になります。

5ー4.分割領域の寸法など

5ー4ー1.行数・列数

分割した領域の内、見出し部分になる「固定列」「固定行」の列数・行数についてです。図17のように「SplitColumn」プロパティで列数が、「SplitRow」プロパティで行数が取得できます。
尚、ウィンドウの分割に於いても、同じです。
分割した行数・列数
図17


分割していない場合は「SplitColumn」および「SplitRow」プロパティとも、ゼロとなります。
分割している場合でも上下のみや左右のみの場合も、見出しになっていない側のプロパティ値はゼロです。

5ー4-2.非可視領域の距離

Excelに備わっている「ウィンドウ枠固定」や「ウィンドウ分割」の機能を使用してしまうと、ドキュメント座標上の位置をスクリーン座標上の位置に置換することが、すぐにはできません。分割し非可視状態になっている領域の分を、ドキュメント座標上では計算してくれますが、スクリーン座標側では分からないからです。
ここでは、今まで説明してきた「領域の数」「各領域での1番目のセル」「見出し部分の列数・行数」を使い、ドキュメント座標上の位置とスクリーン座標上の位置の換算を行います。

まず、図18の左側のようなワークシートを考えます。上下分割を行い、下側の領域をスクロールしている状態です。4~5行目が非可視状態になっています。
非可視領域の距離(上下分割)
図18


図18の左側のように、上側領域の左上角セルは「ActiveWindow.Panes(1).VisibleRange.Cells(1) ①」です。ここではA1セルです。
また下側領域の左上角セルは「ActiveWindow.Panes(2).VisibleRange.Cells(1) ②」です。ここではA6セルです。
見出し行の行数は「ActiveWindow.SplitRow」で得られ、ここでは「3」です。なお左右分割はしていない為「ActiveWindow.SplitColumn」は「ゼロ」となります。

非可視領域を開いた状態が図18の右側です。「上側領域の左上角セル①」を「Offset(ActiveWindow.SplitRow, ActiveWindow.SplitColumn)」だけ移動します。ここでは「Offset(3, 0)」となりますので、移動先は「非可視領域の先頭セル③(A4セル)」となります。

②と③のセルを使って「②.Top - ③.Top」とすることで「非可視領域の高さ」が得られます。
(ちなみに図18の場合には「②.Left - ③.Left」の値はゼロとなります。)

図18は上下分割でしたが、左右分割でも同様の計算が出来ます。図19の左側ではC列・D列が非可視領域となっています。
非可視領域の距離(左右分割)
図19


左側領域の左上角セル(A1セル)は「ActiveWindow.Panes(1).VisibleRange.Cells(1) ①」です。また右側領域の左上角セル(E1セル)は「ActiveWindow.Panes(2).VisibleRange.Cells(1) ②」です。
見出し列の列数は「ActiveWindow.SplitColumn」で得られ、ここでは「2」です。上下分割はしていない為「ActiveWindow.SplitRow」は「ゼロ」となります。

非可視領域を開いた状態が図19の右側です。「左側領域の左上角セル①」を「Offset(ActiveWindow.SplitRow, ActiveWindow.SplitColumn)」だけ移動します。ここでは「Offset(0, 2)」となりますので、移動先は「非可視領域の先頭セル③(C1セル)」となります。

②と③のセルを使って「②.Left - ③.Left」とすることで「非可視領域の幅」が得られます。
(ちなみに図19の場合には「②.Top - ③.Top」の値はゼロとなります。)

上下左右を分割した場合が図20です。図20の左側では、4~5行目とC列・D列が非可視領域となっています。
非可視領域の距離(上下・左右分割)
図20


左上側領域の左上角セル(A1セル)は「ActiveWindow.Panes(1).VisibleRange.Cells(1) ①」です。また右下側領域はPanes(4)なので、左上角セルは「ActiveWindow.Panes(4).VisibleRange.Cells(1) ②」となります。

見出し行の行数は「ActiveWindow.SplitRow」、見出し列の列数は「ActiveWindow.SplitColumn」で得られるので、「左上側領域の左上角セル①」を「Offset(ActiveWindow.SplitRow, ActiveWindow.SplitColumn)」だけ移動します。ここでは「Offset(3, 2)」となりますので、移動先は図20の右側のように「非可視の交わった領域の先頭セル ③(C4セル)」となります。

②と③のセルを使って「非可視領域の距離」を求めます。X方向は「②.Left - ③.Left」、Y方向は「②.Top - ③.Top」となります。

5ー4ー3.分割部の幅・高さ

(説明の順番が少し前後している感じがしますが、説明を分かり易くする為なので御了承下さい。)
分割した領域の内、見出し部分になる「固定列」「固定行」の幅寸法・高さ寸法は、図21のように「SplitHorizontal」プロパティで幅が、「SplitVertical」プロパティで高さが取得できます。尚、ウィンドウの分割に於いても、同じです。
分割した寸法
図21


この「SplitHorizontal」「SplitVertical」で取得できる値は、Microsoftのサイト等でも「ポイント単位で」と説明されていますが、色々試していて違和感を感じたので、改めて調べてみたのが図22です。
表示倍率・ディスプレイ拡大率違いでの見出し部幅と高さ
図22


図22は、表示倍率及びディスプレイ拡大率を変化させた時の「SplitHorizontal」「SplitVertical」を調べたものです。見出し部として今回は3列×2行としています。また測定は細工をしていないワークシートを使ったのでセル幅・高さは標準となりますが、その列幅・行高さも併せて取得しています。

結論から言うとSplitHorizontalとSplitVerticalで得られる値は、分割列幅・行高さの正しいポイント値に「Exceの表示倍率」を掛けた値が戻って来ているようです。ですので、正しくは以下のようになります。
 見出し列の幅(単位:ポイント)= ActiveWindow.SplitHorizontal ÷ Excel表示倍率
 見出し行の幅(単位:ポイント)= ActiveWindow.SplitVertical ÷ Excel表示倍率
なお、ディスプレイ拡大率の影響は無いようですが、計算してみると表示倍率により「多少の誤差」はあるようです。

ちなみに、行高さ・列幅操作の時に表示されるポイント値・ピクセル値(図22の緑色文字)は、実体を表しているようです、
また分割していない場合は「SplitHorizontal」および「SplitVertical」プロパティとも、ゼロとなります。
分割している場合でも上下のみや左右のみの場合も、見出しになっていない側のプロパティ値はゼロです。

上記のように「Excel表示倍率を掛ける」手法以外にも、図20の途中過程で使用した「左上領域の1番目セル」と「そのセルから見出し行数・列数だけ下がったセル(=非可視領域の先頭セル)」で取得する方法が図23です。
見出し部幅と高さをセル位置から取得
図23


分割された領域の内「左上領域の1番目のセル」は「ActiveWindow.Panes(1).VisibleRange.Cells(1) ①」で表されます。図23では、C3セルに相当します。またタイトル部の行数は「ActiveWindow. SplitRow」で、列数は「ActiveWindow.SplitColumn」で取得できます。
そのため非可視領域の先頭セルは、図23の右側のように「ActiveWindow.Panes(1).VisibleRange.Cells(1).Offset(.SplitRow, .SplitColumn) ③」で表せます。
この①と③のLeft値・Top値の差がタイトル部の列幅・行幅となります。

なお、③のみでも良いように思えてしまいますが、図24のようにタイトル部と言えどもA1セル以外からスタートすることも可能ですので、③.Left - ①.Left 等という差分計算が必要です。
見出し部幅と高さをセル位置から取得
図24


この計算方法のメリットとして「分割線の位置」が取得できる事です。
すぐ下の「分割時の補正の考え方」でも説明しますが、見出し部の幅と高さを計算する過程で得る「非可視領域の先頭セル」の位置が、そのまま「分割線」の位置となりますので、Left値・Top値を取得してスクリーン座標に変換さえすれば、例えば「マウスが分割線を越えたか否か」が判別できることになります。

5ー5.分割時の補正の考え方

補正の考え方を示したのが図25です。Excel上のオブジェクト(図25ではRange("F7") )の位置をスクリーン座標上で求める手順を図にしています。ワークシートはウィンドウ分割されており、対象のオブジェクトはPanes(4)に表示されています。
ウィンドウ分割での位置補正
図25


X方向とY方向は分けて考えます。またポイントとピクセルの単位換算は一旦無視して説明します。
X方向(図25の左側)では、まずスクリーン座標原点からドキュメント座標原点(A1セル左上角)までの距離を「ActiveWindow.Panes(1).PointsToScreenPixelsX(0)」で求めます。そのドキュメント座標原点からF7セルまでの距離は「Range("F7").Left」で求められます。
しかしその距離を直接足し合わせる訳にはいきません。「Range("F7").Left」の値には、ウィンドウ分割で見えていない距離も含まれているからです。そのため分割線より右側にあるものは、図19で説明した「非可視領域の幅」を差し引いてから足す必要があります。

その「非可視領域の幅」の処理有無を判断するのが「分割線の位置」となります。これは図23図24で説明した通り、非可視領域の先頭セル「ActiveWindow.Panes(1).VisibleRange.Cells(1).Offset(.SplitRow, .SplitColumn)」のLeft値を取得すれば良いことになります。

Y方向(図25の右側)も同様に、分割線よりも下側にあるものは、図18で説明した「非可視領域の高さ」を差し引きます。

分割線の値を取得するセルとして、図18及び図19では領域数が2であるために「Panes(2)」の先頭セルを、図20では領域数が4であるために「Panes(4)」の先頭セルを①として計算する必要がありますが、Panesの引数に領域数を使い「Panes(ActiveWindow.Panes.Count)」とすることで、どのような分割でも対応できるようになります。

なお「ウィンドウの分割」では、分割線の太さ(約4ポイント)があるため、分割線を越えた領域では若干誤差が発生します。(「ウィンドウ枠の固定」では、分割線の太さは無さそうです)

6.換算の方法

「スクリーン座標~ドキュメント座標」及び「ピクセル単位~ポイント単位」の変換には、ドキュメント座標原点計算及び「72/96(=0.75)」という係数だけで無く、「ディスプレイ拡大率」「表示倍率」「ウィンドウ枠の固定・分割」も考慮が必要である事を説明してきました。
では、これらをどの様に数式に盛り込めば変換ができるのかをまとめたのが図26です。

ウィンドウ分割での位置補正
図26


図26は、横軸が2つの「座標」、縦軸が2つの「単位」で作られた、4つのエリアに分かれています。
まずマウスも含めた各オブジェクトは、「スクリーン座標」上で動くものか、「ドキュメント座標」上で動くものかに区別できます。スクリーン座標上のものはマウス・Excel本体・ユーザーフォーム等です。またドキュメント座標上のものはセルやシート上に貼り付けた図形などです。

またこれらのオブジェクトは、位置を取得・設定する単位として「ピクセル」と「ポイント」の2つに更に分けられます。マウスの位置やExcel本体の位置をPointsToScreenPixelsX/Yメソッドで取得する際には「ピクセル」を使いますし、ユーザーフォームの位置には「ポイント」を使います。
ドキュメント座標上のオブジェクト(セルや図形など)は、私の知る限りは「ポイント」単位で取得・設定されます。
(但し、Excelワークシートの行高さや列幅を変更しようとマウスクリックした時には、ピクセル値も併記されます。)

この分別されたエリアを越えてオブジェクト同士の位置変換を行う際には、ディスプレイ拡大率やExcel表示倍率を考慮する必要が出てきます。図26では、縦の移動(ピクセル⇔ポイント)の時に考慮するもの(「ディスプレイ拡大率」×「96/72 換算値」)を黄色帯、また横の移動(スクリーン座標⇔ドキュメント座標)の時に考慮するもの(「Excelの表示倍率」)を緑色帯で示しました。

また黄色帯・緑色帯を跨ぐ方向の違いで、矢印に「×(乗算)」か「÷(除算)」印をつけています。
例えば現在のユーザーフォームの位置(図26の左下のエリア)を測定して、その位置にマウス(図26の左上のエリア)を合わせようとした場合は、ポイントエリアからピクセルエリアに移る際「黄色帯」を跨ぎます。その時の矢印の記号は「×(乗算)」なので、
 マウス位置=ユーザーフォームの位置 × (ディスプレイ拡大率×(96/72))
という数式を作れば良いという意味になります。
逆にマウスの位置にユーザーフォームを合わせようとすれば「÷(除算)」印側の矢印なので、以下の式になります。
 ユーザーフォーム位置=マウスの位置 ÷ (ディスプレイ拡大率×(96/72))
もちろん、右辺の後ろ側のカッコを外して「マウスの位置 ÷ ディスプレイ拡大率 ÷ (96/72)」としてもOKです。

またシート上の図形の位置(図26の右下エリア)を測定して、その位置にマウス(図26の左上のエリア)を合わせようとする場合は、まず緑色帯の「表示倍率」を「×(乗算)」印で跨ぎ、続いて黄色帯の「ディスプレイ拡大率 ×(96/72)」を「×(乗算)」印で跨ぎます。ですので、
 マウス位置=図形位置 × 表示倍率 × ディスプレイ拡大率×(96/72)
となります。但しドキュメント座標の基準を「Excel原点位置」から求める必要があります。その時、マウス位置とExcel原点位置は同じエリア内(図26の左上エリア)にありますので、Excel原点位置は「そのままの状態」で使い、
 マウス位置=Excel原点位置+図形位置×表示倍率×ディスプレイ拡大率×(96/72)
という数式になります。

なお、ウィンドウ分割やウィンドウ枠の固定を行っている場合には、非可視領域の距離を考慮(青色帯)する必要があります。
ここを跨ぐ時にはドキュメント座標上の位置に対して非可視領域の距離を足したり引いたりする必要がありますので、図26では跨ぐ方向の矢印に「+(加算)」と「-(減算)」印を付けてあります。
つまり上記の例(図形位置にマウスを合わせる)だと、まず図形位置から非可視領域の距離を引き、その後で表示倍率・ディスプレイ拡大率・換算係数を掛けるという事になります。つまり、
 マウス位置=Excel原点位置+図形位置-非可視領域の距離 )×表示倍率×ディスプレイ拡大率×(96/72)
となります。

なお図26には「ドキュメント座標×ピクセル単位」に属するオブジェクトはありませんが、もし存在するのであれば青色帯相当の「ウィンドウ分割・固定の非可視領域の距離のピクセル単位」を考慮する必要が出てくるものと思われます。

7.ツールの紹介

今回紹介するツール(「サンプルファイル」)は、上で説明した変換式が正しいか否かの確認をしたものです。実務で役に立つかどうかは分かりませんので御了承下さい。

7ー1.概要

ツールのワークシート(Sheet1)上には、図27のように複数のボタンと図形を1つ並べてあります。
ツールの機能
図27


「UF」と書かれたボタン①(一番左上)をクリックすると、ユーザーフォーム②がモードレスで起動します。起動したユーザーフォームは、図28のように左上角で位置を測定するものです。
ユーザーフォーム上には「スクリーン座標」及び「ドキュメント座標」でのX方向Y方向の位置が「ポイント単位」「ピクセル単位」で表示され、その下に「セルのアドレス」または「図形やコントロールの名前」が表示されます。
一番下には「PCのディスプレイ拡大率」と「Excelのシートの表示倍率」を表示しています。
測定用ユーザーフォーム上の機能
図28


シート上の表枠内の全6個のボタン(図27の③)は、図29のように丸数字の部分を選択実行するようにしています。
測定側
スクリーン座標ドキュメント座標
ピクセルポイントポイント
マウスUserFormセル、図形
求める量スクリーン座標ピクセルマウス
ポイントUserForm
ドキュメント座標ポイントセル、図形
図29


図29の横軸が測定側で、縦軸が求める量側としています。例えば図29の⑥のボタンをクリックすると、シート上の「セル・図形(今回プログラムでは"図形01"の四角図形④)」の位置を調べ、内部で換算をした後「マウス」の位置を図形のところに移動するようにしています。
逆に⑨のボタンの場合は、「マウス」がある位置にシート上の図形を移動してきます。他のボタンも同様の動きをします。

但しユーザーフォームが対象となっているボタン(⑤⑦⑧⑩)の場合は、上記「UFボタン①」でUserForm1を事前起動しておかないと、「スクリーン座標原点にUserFormがある」事として動作するか、若しくは無視される事になりますので御注意下さい。

なお、図47で説明する「フォーム左上角をピッタリ合わせる」処理は、③の6個のボタンには対応させていないので、移動後でもちょっとだけズレがありますが御了承下さい。

7ー2.ワークシート

7ー2ー1.シート上のレイアウト

ワークシート(Sheet1)上には、図29の表を作り、そこに6つのボタン(フォームコントロール)を配置します。シート左上側にもボタン(「UF」とのCaptionを設定)を1つ配置しています。
シート上のレイアウト
図30


全7個のボタンには、図30のようにマクロを登録します。またシートの適当な場所に図形を貼り付け、その図形の名前を「"図形01"」としておきます。この名前は、図33等のプロシージャ内で変数設定値となります。

7ー2ー2.シートモジュール

シート上の7個のボタンから実行されるプロシージャは、全てシートモジュールに置きました。

7ー2ー2ー1.①測定用ユーザーフォームの起動
図30のボタン①をクリックした時に実行されるのが図31です。
02行目「UserForm1.Show 0」で、UserForm1をモードレス(フォーム起動中もシート操作可)で起動します。
  1. '========== ⇩(1) ユーザーフォームの起動 ============
  2. Sub UFstart()
  3.  UserForm1.Show 0
  4. End Sub
図31


7ー2ー2ー2.⑤ユーザーフォームの位置にマウスを移動
図30の⑤ボタン(スクリーン座標のポイント単位 → スクリーン座標のピクセル単位)をクリック時に実行されるのが図32です。配置したユーザーフォームの位置にマウスを移動させます。
  1. '========== ⇩(2) ユーザーフォームの位置にマウスを移動 ============
  2. Sub Disp_pt2Disp_px()
  3.  Dim X As Single   '←X方向のポイント・ピクセル
  4.  Dim Y As Single   '←Y方向のポイント・ピクセル
  5.  X = UserForm1.Left
  6.  Y = UserForm1.Top
  7.  X = X * (96 / 72) * (LogicalPixcel / 96)
  8.  Y = Y * (96 / 72) * (LogicalPixcel / 96)
  9.  Call SetCursorPos(CLng(x), CLng(y))
  10. End Sub
図32


15行目「X = UserForm1.Left」、16行目「Y = UserForm1.Top」で、現在のユーザーフォームの位置を取得し、変数X、Yに代入します。この段階ではポイント単位です。

18行目「X = X * (96 / 72) * (LogicalPixcel / 96)」で、ポイント単位をピクセル単位に変換し、且つディスプレイ拡大率を乗算します。これは図26で言う「スクリーン座標のポイント単位」から「スクリーン座標のピクセル単位」にエリアが移る際、「ディスプレイ拡大率 × (96/72)」を掛けている(矢印の方向が「×(乗算)」印)ことを表しています。
なお、別な表し方として「X = X / 0.75 * (LogicalPixcel / 96)」でもOKです。
19行目「Y = Y * (96 / 72) * (LogicalPixcel / 96)」ではY方向を変換しています。

21行目「Call SetCursorPos(CLng(x), CLng(y))」では、18~19行目で変換した値を引数にして、マウス位置を変更しています。その際、SetCursorPosへの引数はLong型にする必要があるので「CLng関数」で型変換をしています。
なお、SetCursorPosは「Win32 API関数」ですので、標準モジュールの先頭部分(図38の247行目、255行目)で宣言をしています。

7ー2ー2ー3.⑥シート上の図形の位置にマウスを移動
図30の⑥ボタン(ドキュメント座標のポイント単位 → スクリーン座標のピクセル単位)をクリック時に実行されるのが図33です。シート上に配置した図形の位置にマウスを移動させます。
  1. '========== ⇩(3) シート上の図形の位置にマウスを移動 ============
  2. Sub Doc2Disp_px()
  3.  Dim X As Single     '←X方向のポイント・ピクセル
  4.  Dim Y As Single     '←Y方向のポイント・ピクセル
  5.  Dim Pane2 As Range   '←左上領域の1番目のセル
  6.  Dim Pane3 As Range   '←非可視領域の1番目のセル
  7.  Dim T As Object     '←移動先目標とするオブジェクト(今回は図形)
  8.  Set T = Sheet1.Shapes("図形01")
  9.  X = T.Left
  10.  Y = T.Top
  11.  With ActiveWindow
  12.   Set Pane2 = .Panes(.Panes.Count).VisibleRange.Cells(1)
  13.   Set Pane3 = .Panes(1).VisibleRange.Cells(1).Offset(.SplitRow, .SplitColumn)
  14.   If X > Pane3.Left Then
  15.    X = X - (Pane2.Left - .Pane3.Left)
  16.   End If
  17.   If Y > Pane3.Top Then
  18.    Y = Y - (Pane2.Top - Pane3.Top)
  19.   End If
  20.   X = X * .Zoom / 100
  21.   Y = Y * .Zoom / 100
  22.   X = .Panes(1).PointsToScreenPixelsX(0) + X * (96 / 72) * (LogicalPixcel / 96)
  23.   Y = .Panes(1).PointsToScreenPixelsY(0) + Y * (96 / 72) * (LogicalPixcel / 96)
  24.  End With
  25.  Call SetCursorPos(CLng(x), CLng(y))
  26.  Set Pane2 = Nothing
  27.  Set Pane3 = Nothing
  28.  Set T = Nothing
  29. End Sub
図33


38行目「Set T = Sheet1.Shapes("図形01")」では、図形オブジェクトを変数Tに設定します。
40行目「X = T.Left」、41行目「Y = T.Top」では、移動先目標である図形の位置を、まずドキュメント座標上のポイント単位で変数X、Yに代入します。

43行目「With ActiveWindow」は、44~59行目を「ActiveWindow(現在表示しているシート)」基準とします。
このプロシージャでは、図26の右下エリアから左上エリアへの移動ですので、まず非可視領域の距離補正をし、次に表示倍率、その次にディスプレイ拡大率と単位変換をします。

まず、図20の②相当のセルを取得するため、44行目「Set Pane2 = .Panes(.Panes.Count).VisibleRange.Cells(1)」で、右下エリアの先頭セルをPane2に設定します。
続けて、図20の③相当のセルを取得するため、45行目「Set Pane3 = .Panes(1).VisibleRange.Cells(1).Offset(.SplitRow, .SplitColumn)」で、非可視領域の先頭セルをPane3に設定します。

次に非可視領域の距離の補正です。
47行目「If X > Pane3.Left Then」では、図形の位置が分割線のX方向を超えているか否かを調べています。
越えている(図形が分割線よりも右にある)場合は、48行目「X = X - (Pane2.Left - .Pane3.Left)」で、非可視領域の幅を元の図形の位置から引いています。「-(マイナス)」としているのは、、図26で矢印の方向が「-(減算)」となっているところから来ています。

51行目「If Y > Pane3.Top Then」では、図形の位置が分割線のY方向を超えているか否かを調べ、越えている(図形が分割線よりも下にある)場合は、52行目「Y = Y - (Pane2.Top - Pane3.Top)」で非可視領域の高さを元の図形の位置から引いています。

非可視領域の補正が完了したら、次はドキュメント座標からスクリーン座標への「表示倍率」の変換です。
55行目「X = X * .Zoom / 100」では、48行目で補正した変数Xに対して、Excel表示倍率を補正しています。図26で矢印の方向が「×(乗算)」となっていますので、表示倍率(.Zoom / 100)を掛けています。
56行目「Y = Y * .Zoom / 100」もY方向に対して表示倍率を掛けています。

スクリーン座標への変換が完了したら、ポイント単位→ピクセル単位への変換と併せて、ドキュメント座標原点の補正が必要です。
58行目「X = .Panes(1).PointsToScreenPixelsX(0) + X * (96 / 72) * (LogicalPixcel / 96)」の右辺の後半「X * (96 / 72) * (LogicalPixcel / 96)」では、ポイント単位からピクセル単位への変換を行っています。
そしてディスプレイ座標原点の補正については、終点がExcel原点と同じ左上エリアであることから「ActiveWindow.Panes(1).PointsToScreenPixelsX(0)」をそのまま足し合わせます。
59行目「Y = .Panes(1).PointsToScreenPixelsY(0) + Y * (96 / 72) * (LogicalPixcel / 96)」も同様にY方向の補正をしています。

補正が完了したら、62行目「Call SetCursorPos(CLng(x), CLng(y))」で、API関数SetCursorPosの引数に設定し、マウスの位置を図形の位置に移動させます。

最後に、64行目「Set Pane2 = Nothing」、65行目「Set Pane3 = Nothing」で、44~45行目で設定したRangeオブジェクトをクリアし、66行目「Set T = Nothing」で目標図形オブジェクトをクリアしています。

7ー2ー2ー4.⑦マウスの位置にユーザーフォームを移動
図30の⑦ボタン(スクリーン座標のピクセル単位 → スクリーン座標のポイント単位)をクリック時に実行されるのが図34です。マウスの位置にユーザーフォームを移動させます。
  1. '========== ⇩(4) マウスの位置にユーザーフォームを移動 ============
  2. Sub Disp_px2Disp_pt()
  3.  Dim c As cPoint   '←マウス位置の構造体
  4.  Dim X As Single   '←X方向のピクセル
  5.  Dim Y As Single   '←Y方向のピクセル
  6.  Call GetCursorPos(c)
  7.  X = c.X / (96 / 72) / (LogicalPixcel / 96)
  8.  Y = c.Y / (96 / 72) / (LogicalPixcel / 96)
  9.  UserForm1.Left = X
  10.  UserForm1.Top = Y
  11. End Sub
図34


76行目「Call GetCursorPos(c)」では、Win32APIのGetCursorPos関数を呼び出し、現在のマウス位置を取得します。このAPI関数は、標準モジュールの先頭部分(図38の248行目・256行目)で、また構造体cPointも図39の265~268行目で宣言しています。

取得したマウス位置は、X方向は「c.x(ピクセル単位)」Y方向は「c.y(ピクセル単位)」で取り出せます。
78行目「X = c.X / (96 / 72) / (LogicalPixcel / 96)」では、ユーザーフォームの位置(スクリーン座標のポイント単位)に換算しています。図26で言えば同じスクリーン座標上で「ピクセル単位→ポイント単位」ですので矢印の向きは「÷(除算)」です。そのため「c.X / (ディスプレイ拡大率 × (96/72) )」となります。
79行目「Y = c.Y / (96 / 72) / (LogicalPixcel / 96)」はY方向ですが、同じ内容です。

その値を使って、81行目「UserForm1.Left = X」、82行目「UserForm1.Top = Y」で、ユーザーフォームの位置を移動させます。

7ー2ー2ー5.⑧図形の位置にユーザーフォームを移動
図30の⑧ボタン(ドキュメント座標のポイント単位 → スクリーン座標のポイント単位)をクリック時に実行されるのが図35です。図形の位置にユーザーフォームを移動させます。
  1. '========== ⇩(5) 図形の位置にユーザーフォームを移動 ============
  2. Sub Doc2Disp_pt()
  3.  Dim X As Single     '←X方向のポイント
  4.  Dim Y As Single     '←Y方向のポイント
  5.  Dim Pane2 As Range   '←左上領域の1番目セル
  6.  Dim Pane3 As Range   '←非可視領域の1番目セル
  7.  Dim T As Object     '←移動目標とするオブジェクト(今回は図形)
  8.  Set T = Sheet1.Shapes("図形01")
  9.  X = T.Left
  10.  Y = T.Top
  11.  With ActiveWindow
  12.   Set Pane2 = .Panes(.Panes.Count).VisibleRange.Cells(1)
  13.   Set Pane3 = .Panes(1).VisibleRange.Cells(1).Offset(.SplitRow, .SplitColumn)
  14.   If X > Pane3.Left Then
  15.    X = X - (Pane2.Left - Pane3.Left)
  16.   End If
  17.   If Y > Pane3.Top Then
  18.    Y = Y - (Pane2.Top - Pane3.Top)
  19.   End If
  20.   X = X * .Zoom / 100
  21.   Y = Y * .Zoom / 100
  22.   X = .Panes(1).PointsToScreenPixelsX(0) / (96 / 72) / (LogicalPixcel / 96) + X
  23.   Y = .Panes(1).PointsToScreenPixelsY(0) / (96 / 72) / (LogicalPixcel / 96) + Y
  24.  End With
  25.  UserForm1.Left = X
  26.  UserForm1.Top = Y
  27.  Set Pane1 = Nothing
  28.  Set Pane2 = Nothing
  29.  Set T = Nothing
  30. End Sub
図35


98行目「Set T = Sheet1.Shapes("図形01")」では、図形を変数Tに設定します。
100行目「X = T.Left」、101行目「Y = T.Top」では、移動先目標である図形の位置を変数X、Yに代入します。

103行目「With ActiveWindow」は、104~119行目を「ActiveWindow(現在表示しているシート)」基準とします。
このプロシージャでは、図26の右下エリアから左下エリアへの移動ですので、まず非可視領域の距離補正をし、次に表示倍率の換算をします。

まず、図20の②相当のセルを取得するため、104行目「Set Pane2 = .Panes(.Panes.Count).VisibleRange.Cells(1)」で、右下エリアの先頭セルをPane2に設定します。
続けて、図20の③相当のセルを取得するため、105行目「Set Pane3 = .Panes(1).VisibleRange.Cells(1).Offset(.SplitRow, .SplitColumn)」で、非可視領域の先頭セルをPane3に設定します。

次に非可視領域の距離の補正です。
107行目「If X > Pane3.Left Then」では、図形の位置が分割線を左右方向に越えているか否かを調べています。
越えている(図形が分割線よりも右にある)場合は、108行目「X = X - (Pane2.Left - .Pane3.Left)」で、非可視領域の幅を元の図形の位置から引いています。これは、図26で矢印の方向が「-(減算)」となっているためです。

111行目「If Y > Pane3.Top Then」では、図形の位置が分割線を上下方向に越えているか否かを調べ、越えている(図形が分割線よりも下にある)場合は、112行目「Y = Y - (Pane2.Top - Pane3.Top)」で非可視領域の高さを元の図形の位置から引いています。

非可視領域の補正が完了したら、次はドキュメント座標からスクリーン座標への「表示倍率」の変換です。
115行目「X = X * .Zoom / 100」では、108行目で補正した変数Xに対して、Excel表示倍率を補正しています。図26で矢印の方向が「×(乗算)」となっていますので、表示倍率(.Zoom / 100)を掛けています。
116行目「Y = Y * .Zoom / 100」もY方向に対して表示倍率を掛けています。

118行目「X = .Panes(1).PointsToScreenPixelsX(0) / (96 / 72) / (LogicalPixcel / 96) + X」ではドキュメント座標原点の補正をします。今回の終点は「スクリーン座標 × ポイント」エリアですので、図26で「Excel原点位置」をポイント単位のエリアに移す必要があります。矢印の向きは「÷(除算)」になっていますので、「ディスプレイ拡大率 × (96/72)」を割り算することが分かります。その割り算の結果を115行目で得たX値に足します。

補正が完了したら、122行目「UserForm1.Left = X 」、123行目「UserForm1.Top = Y 」で、ユーザーフォームの位置を図形の位置に移動させます。

7ー2ー2ー6.⑨マウスの位置に図形を移動
図30の⑨ボタン(スクリーン座標のピクセル単位 → ドキュメント座標のポイント単位)をクリック時に実行されるのが図36です。マウスの位置に図形を移動させます。
  1. '========== ⇩(6) マウスの位置に図形を移動 ============
  2. Sub Disp_px2Doc()
  3.  Dim X As Single     '←X方向のポイント
  4.  Dim Y As Single     '←Y方向のポイント
  5.  Dim Pane2 As Range   '←左上領域の1番目セル
  6.  Dim Pane3 As Range   '←非可視領域の1番目セル
  7.  Dim T As Object     '←移動対象のオブジェクト(今回は図形)
  8.  Dim c As cPoint     '←マウス位置の構造体
  9.  Set T = Sheet1.Shapes("図形01")
  10.  Call GetCursorPos(c)
  11.  With ActiveWindow
  12.   X = c.X - .Panes(1).PointsToScreenPixelsX(0)
  13.   Y = c.Y - .Panes(1).PointsToScreenPixelsY(0)
  14.   X = X / (96 / 72) / (LogicalPixcel / 96)
  15.   Y = Y / (96 / 72) / (LogicalPixcel / 96)
  16.   X = X / (.Zoom / 100)
  17.   Y = Y / (.Zoom / 100)
  18.   Set Pane2 = .Panes(.Panes.Count).VisibleRange.Cells(1)
  19.   Set Pane3 = .Panes(1).VisibleRange.Cells(1).Offset(.SplitRow, .SplitColumn)
  20.   If X > Pane3.Left Then
  21.    X = X + (Pane2.Left - Pane3.Left)
  22.   End If
  23.   If Y > Pane3.Top Then
  24.    Y = Y + (Pane2.Top - Pane3.Top)
  25.   End If
  26.  End With
  27.  T.Left = X
  28.  T.Top = Y
  29.  Set Pane2 = Nothing
  30.  Set Pane3 = Nothing
  31.  Set T = Nothing
  32. End Sub
図36


149行目「Set T = Sheet1.Shapes("図形01")」では、図形を変数Tに設定します。
151行目「Call GetCursorPos(c)」では、Win32APIのGetCursorPos関数を呼び出し、現在のマウス位置を取得します。このAPI関数宣言は、標準モジュールの先頭部分(図38の248行目・256行目)で、また構造体cPointも図39の265~268行目で宣言しています。

153行目「With ActiveWindow」は、154~172行目を「ActiveWindow(現在表示しているシート)」基準とします。
このプロシージャでは、図26の左上エリアから右上エリアへの移動ですので、まずディスプレイ拡大率補正+単位換算をし、次に表示倍率、その次に非可視領域範囲の補正をします。

151行目で取得したマウス位置に対し、まず154行目「X = c.X - .Panes(1).PointsToScreenPixelsX(0)」でドキュメント座標原点を減算することで、ドキュメント座標相当のピクセル値に変換をします。マウスとExcel原点は、図26では同じ左上エリアなので、PointsToScreenPixelsXはそのままです。
155行目「Y = c.Y - .Panes(1).PointsToScreenPixelsY(0)」は、Y方向の計算です。

次に、図26の左上から左下エリアに移動しますので、「ディスプレイ拡大率 × (96/72)」を割り算します。これが157行目「X = X / (96 / 72) / (LogicalPixcel / 96)」、158行目「Y = Y / (96 / 72) / (LogicalPixcel / 96)」に相当します。
その次に、図26の左下から右下エリアに移動しますので、「表示倍率」を割り算します。これが160行目「X = X / (.Zoom / 100)」、161行目「Y = Y / (.Zoom / 100)」に相当します。
最後に非可視領域の補正をします。まず図20の②、③相当のセル位置を取得します。
163行目「Set Pane2 = .Panes(.Panes.Count).VisibleRange.Cells(1)」で、図20の右下エリアの先頭セル②を取得します。
164行目「Set Pane3 = .Panes(1).VisibleRange.Cells(1).Offset(.SplitRow, .SplitColumn)」で、非可視領域の先頭セル③を取得します。

166行目「If X > Pane3.Left Then」では、マウスの位置が分割線よりも右側にあるか否かを調べ、右側にある時は167行目「X = X + (Pane2.Left - Pane3.Left)」で、非可視領域の距離だけ足します。これは図26の「図形位置に向かう側の矢印が+(加算)」になっているためです。
同様に170行目「If Y > Pane3.Top Then」で、マウスが分割線より下にあるか否かを調べ、下にある場合は171行目「Y = Y + (Pane2.Top - Pane3.Top)」で補正をします。

補正が完了したら、176行目「T.Left = X」177行目「T.Top = Y」で図形の位置を移動させます。

7ー2ー2ー7.⑩ユーザーフォームの位置に図形を移動
図30の⑩ボタン(スクリーン座標のポイント単位 → ドキュメント座標のポイント単位)をクリック時に実行されるのが図37です。ユーザーフォームの位置に図形を移動させます。
  1. '========== ⇩(7) ユーザーフォームの位置に図形を移動 ============
  2. Sub Disp_pt2Doc()
  3.  Dim X As Single     '←X方向のポイント
  4.  Dim Y As Single     '←Y方向のポイント
  5.  Dim Pane2 As Range   '←左上領域の1番目セル
  6.  Dim Pane3 As Range   '←非可視領域の1番目セル
  7.  Dim T As Object     '←移動対象のオブジェクト(今回は図形)
  8.  Set T = Sheet1.Shapes("図形01")
  9.  X = UserForm1.Left
  10.  Y = UserForm1.Top
  11.  With ActiveWindow
  12.   X = X - .Panes(1).PointsToScreenPixelsX(0) / (96 / 72) / (LogicalPixcel / 96)
  13.   Y = Y - .Panes(1).PointsToScreenPixelsY(0) / (96 / 72) / (LogicalPixcel / 96)
  14.   X = X / (.Zoom / 100)
  15.   Y = Y / (.Zoom / 100)
  16.   Set Pane2 = .Panes(.Panes.Count).VisibleRange.Cells(1)
  17.   Set Pane3 = .Panes(1).VisibleRange.Cells(1).Offset(.SplitRow, .SplitColumn)
  18.   If X > Pane3.Left Then
  19.    X = X + (Pane2.Left - Pane3.Left)
  20.   End If
  21.   If Y > Pane3.Top Then
  22.    Y = Y + (Pane2.Top - Pane3.Top)
  23.   End If
  24.  End With
  25.  T.Left = X
  26.  T.Top = Y
  27.  Set Pane2 = Nothing
  28.  Set Pane3 = Nothing
  29.  Set T = Nothing
  30. End Sub
図37


198行目「Set T = Sheet1.Shapes("図形01")」では、図形を変数Tに設定します。
200行目「X = UserForm1.Left」、201行目「Y = UserForm1.Top」では、現在のユーザーフォームの位置を取得します。

203行目「With ActiveWindow」は、204~219行目を「ActiveWindow(現在表示しているシート)」基準とします。
このプロシージャでは、図26の左下エリアから右下エリアへの移動ですので、まず表示倍率の換算をし、次に非可視領域の距離補正をします。

表示倍率換算の前に、Excel原点の処理を204行目「X = X - .Panes(1).PointsToScreenPixelsX(0) / (96 / 72) / (LogicalPixcel / 96)」で行います。計測するエリアが左下ですので「Excel原点に対して「ディスプレイ拡大率 × (96/72)」を割り算した値を引算しています。
205行目「Y = Y - .Panes(1).PointsToScreenPixelsY(0) / (96 / 72) / (LogicalPixcel / 96)」はY方向です。

Excel原点の処理が完了したら、207行目「X = X / (.Zoom / 100)」、208行目「Y = Y / (.Zoom / 100)」で表示倍率の換算をします。図26でも矢印の方向が「÷(除算)」になっていますので除算をします。

最後に非可視領域の距離を足していきます。
213行目「If X > Pane3.Left Then」では、ユーザーフォームの位置が分割線を左右方向に越えているか否かを調べています。越えている(ユーザーフォームが分割線よりも右にある)場合は、214行目「X = X + (Pane2.Left - Pane3.Left)」で、非可視領域の幅を足しています。
Y方向は217行目「If Y > Pane3.Top Then」でユーザーフォームの位置が分割線を上下方向に越えているか否かを調べ、越えていれば218行目「Y = Y + (Pane2.Top - Pane3.Top)」で非可視領域の高さを足しています。

計算結果を使い、223行目「T.Left = X」、224行目「T.Top = Y」で図形位置を変更しています。

7ー3.標準モジュール(Module1)

7ー3ー1.Win32 API関数の宣言

先頭部(宣言部)では、ディスプレイ拡大率を取得するための関数4種、およびマウスの位置取得と設定の関数各1種を宣言しています。尚、Excelが64ビットの時と32ビットの時で宣言の書式が異なりますので、#If~#Else~#End Ifで分岐させています。
  1. '========== ⇩(8) Win32 API関数の宣言 ============
  2. #If Win64 Then
  3.  Private Declare PtrSafe Function GetDesktopWindow Lib "User32" () As Long
  4.  Private Declare PtrSafe Function GetDC Lib "User32" (ByVal hWnd As Long) As Long
  5.  Private Declare PtrSafe Function GetDeviceCaps Lib "gdi32" (ByVal hdc As Long, ByVal nIndex As Long) As Long
  6.  Private Declare PtrSafe Function ReleaseDC Lib "User32" (ByVal hWnd As Long, ByVal hdc As Long) As Long
  7.  Public Declare PtrSafe Function SetCursorPos Lib "User32" (ByVal X As Long, ByVal Y As Long) As Long
  8.  Public Declare PtrSafe Function GetCursorPos Lib "User32" (lpPoint As cPoint) As Long
  9. #Else
  10.  Private Declare Function GetDesktopWindow Lib "user32" () As Long
  11.  Private Declare Function GetDC Lib "user32" (ByVal hWnd As Long) As Long
  12.  Private Declare Function GetDeviceCaps Lib "gdi32" (ByVal hdc As Long, ByVal nIndex As Long) As Long
  13.  Private Declare Function ReleaseDC Lib "user32" (ByVal hWnd As Long, ByVal hdc As Long) As Long
  14.  Public Declare Function SetCursorPos Lib "user32" (ByVal x As Long, ByVal y As Long) As Long
  15.  Public Declare Function GetCursorPos Lib "User32" (lpPoint As cPoint) As Long
  16. #End If
図38


ディスプレイ拡大率を取得するには「GetDesktopWindow」「GetDC」「GetDeviceCaps」「ReleaseDC」の4種の関数が必要ですので、242~245行目で宣言しています。32ビットの場合は250~253行目になります。

マウス位置の取得・設定を行うのが247~248行目の「SetCursorPos」「GetCursorPos」関数です。32ビットでは255~256行目で宣言しています。

なおディスプレイ拡大率関係の宣言はPrivateとしていますが、これは処理を行うプロシージャが同じ標準モジュール内(図40)にあるためです(シートモジュールやフォームモジュールから、標準モジュール上の図40を呼び出す形にしています)。
一方マウスの位置の宣言はPublicで行っています。これは使用するプロシージャがシートモジュールやフォームモジュールにあるためです。

64ビットと32ビットとの宣言の違いは、Functionの前にPtrSafeを付けるか付けないかと併せて、32ビットではLong型のものを64ビットではLongPtr型にする必要がありますが、Long型のままでもOKの様です。もし分けて宣言する場合は、API関数を使用する各プロシージャの中でも#If~#Else~#End Ifで変数のデータ型を分けて宣言しないと、データ型異常のエラーが出ますので注意が必要です。

7ー3ー2.定数宣言とマウス構造体の宣言

ディスプレイ拡大率の取得に必要な値を宣言部で定数宣言しておきます。また、マウスの位置取得に必要な構造体cPointについても宣言します。
  1. '========== ⇩(9) GetDeviceCaps関数の定数値 ============
  2. Private Const LOGPIXELSX As Long = 88   '←GetDeviceCaps関数の定数値(水平方向)
  3. Private Const LOGPIXELSY As Long = 90   '←GetDeviceCaps関数の定数値(垂直方向)
  4. '========== ⇩(10) マウス位置の構造体宣言 ============
  5. Type cPoint
  6.  X As Long
  7.  Y As Long
  8. End Type
図39


261行目「Private Const LOGPIXELSX As Long = 88」、262行目「Private Const LOGPIXELSY As Long = 90」は、図38の244行目・252行目でAPI関数宣言をしているGetDeviceCapsの第二引数に指定する定数です。この第二引数には図10で示した多くの定数を指定でき、様々なディスプレイ情報を得ることができますが、今回はその内の「論理インチ当りの画面のピクセル数」を取得します。
スクリーンの密度はX方向・Y方向で実際には同じですので、「LOGPIXELSX(88)」または「LOGPIXELSY(90)」のどちらでも同じ結果が得られるので、一方のみの定数設定でも問題ないのですが、とりあえず両方向の定数を宣言しています。

265~268行目で構造体宣言をしているcPointは、図38の248行目・256行目でAPI関数宣言をしているGetCursorPosの引数となります。マウス位置設定側の関数(SetCursorPos)は、引数としてX・Y値を渡せるのですが、取得側であるGetCursorPosは構造体に位置が入った形でデータを受け取ることになります。

7ー3ー3.ディスプレイ拡大率の計算

ディスプレイ拡大率は、様々なプロシージャから呼び出して使用するため、図40のようなユーザー定義関数方式としました。
  1. '========== ⇩(11) ディスプレイ拡大率の計算 ============
  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
図40


285行目「hWndDesk = GetDesktopWindow()」では、デスクトップのウィンドウハンドルを取得します。
286行目「hDCDesk = GetDC(hWndDesk)」で、デスクトップのデバイスコンテキストを取得します。
287行目「LogicalPixcel = GetDeviceCaps(hDCDesk, LOGPIXELSX)」では、第二引数に図39の261行目で定数設定した「LOGPIXELSX」を指定して「論理インチ当りの画面のピクセル数(X方向)」を取得し、関数プロシージャの戻り値とします。

289行目「Call ReleaseDC(hWndDesk, hDCDesk)」では、デスクトップのデバイスコンテキストを解放し、他のアプリから使用できるようにしています。
なお、このディスプレイ拡大率(実際にはピクセル数)の取得は、Excel本体を起動した時の値です。Excelを開いている途中でディスプレイ拡大率を変更しても反映されません。その際はExcelを開き直す必要があります。
しかも、このディスプレイ拡大率の取得コードが書かれたExcelファイルを閉じるだけでは不十分で、全てのExcelファイルを閉じてから、再度開くことが必要です。同じVBAコードが動くはずの他のOffice製品(OutlookやWord、PowerPoint)も閉じる必要があるのでは?と思ったのですが、Excel以外はそのままでも大丈夫でした(この辺りが理解できません)。

7ー4.ユーザーフォーム(UserForm1)

シート上の「UFボタン」をクリックすると図31が呼び出され、その中の02行目から起動されるのがUserForm1です。
このユーザーフォームは画面の寸法を計測するのと同時に、シート上の図形・マウスと位置を相互に合わせる役目も担っています。

7ー4ー1.フォーム上レイアウト

フォーム上のレイアウトは図41のようにしました。
フォーム上レイアウト
図41


表示する値は全11種で、対応するLabelの番号は図42の通りです。その他の説明用文字列は適当に並べています。
ポイント単位ピクセル単位
スクリーン
座標
X方向Label1Label3
Y方向Label2Label4
ドキュメント
座標
X方向Label5Label7
Y方向Label6Label8
セルの位置・図形の名前Label9
ディスプレイ拡大率Label10
Excel表示倍率Label11
図42


なお、フォーム終了ボタンは設けていないので、右上×印クリックで終了させます。

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

7ー4ー2ー1.モジュールレベル変数
フォームモジュールの宣言部で、ディスプレイ拡大率(WinScreenRatio)と表示倍率(ZoomRatio)の変数宣言をします。
  1. '========== ⇩(12) フォームモジュールレベル変数の宣言 ============
  2. Dim WinScreenRatio As Double   'ディスプレイ拡大率
  3. Dim ZoomRatio As Double     'Excel表示倍率
図43


301行目、302行目ともにDouble型で宣言をしています。たぶんディスプレイ拡大率はSingle型(1、1.25、1.5、・・のはず)、表示倍率はInteger型(・・・、100、101、102、・・・のはず)で良い気がしますが、調べ切れていないので今回はDouble型で余裕を取っています。

7ー4ー2ー2.フォーム初期設定
起動時に呼び出されるInitializeイベントプロシージャでは、Labelコントロールの書式設定と併せ、ディスプレイ拡大率の設定とフォーム上への表示、及び表示倍率の取得と表示を、図44のように行います。
  1. '========== ⇩(13) フォーム初期設定 ============
  2. Private Sub UserForm_Initialize()
  3.  Me.Label1.TextAlign = fmTextAlignRight
  4.  Me.Label2.TextAlign = fmTextAlignRight
  5.  Me.Label3.TextAlign = fmTextAlignRight
  6.  Me.Label4.TextAlign = fmTextAlignRight
  7.  Me.Label5.TextAlign = fmTextAlignRight
  8.  Me.Label6.TextAlign = fmTextAlignRight
  9.  Me.Label7.TextAlign = fmTextAlignRight
  10.  Me.Label8.TextAlign = fmTextAlignRight
  11.  WinScreenRatio = LogicalPixcel / 96
  12.  Me.Label10.Caption = WinScreenRatio * 100 & "%"
  13.  Call Zoom_Ini
  14. End Sub
図44


312行目「Me.Label1.TextAlign = fmTextAlignRight」では、Label1上に書かれる数字を右寄せにしています。これはLabelは標準では左寄せのため、桁上がりの時に小数点位置が左右に動いてしまう等の理由から数値らしく見えないので、右寄せにしています。数値を入れる他のLabel2~8も同様(313~319行目)に右寄せにしています。

321行目「WinScreenRatio = LogicalPixcel / 96」では、図40のLogicalPixcelユーザー定義関数を呼び出し、論理インチ当りの画面ピクセル数を取得しています。LogicalPixcelの戻り値は、拡大率100%では96が戻ってきますので「96」で割り算をし、ディスプレイ拡大率としています。なおこの値はExcel起動時の値ですので、ディスプレイ拡大率を変更した際にはExcel本体を再立ち上げし直さないと正しい結果は得られません。(他のExcelが立ち上がったままだと、反映されない)
322行目「Me.Label10.Caption = WinScreenRatio * 100 & "%"」では、321行目で計算したディスプレイ拡大率を再びパーセント単位にしてLabel10に書き込んでいます。

323行目「Call Zoom_Ini」は図45を呼び出し、表示倍率を取得しフォーム上に書き込んでいます。これは、フォーム起動直後では図46のLayoutイベントが発生せず、表示倍率が表示されないのを防ぐためです。

7ー4ー2ー3.表示倍率取得・表示
図44の323行目、図46の348行目から呼び出されるのが図45です。Excel表示倍率を取得しLabel11に書き込みます。
  1. '========== ⇩(14) 表示倍率取得・表示 ============
  2. Private Sub Zoom_Ini()
  3.  ZoomRatio = ActiveWindow.Zoom
  4.  Me.Label11.Caption = ZoomRatio & "%"
  5. End Sub
図45


332行目「ZoomRatio = ActiveWindow.Zoom」では、Zoomプロパティで現在のシートの表示倍率を取得し、変数ZoomRatio(モジュールレベル変数)に代入します。表示倍率は、100%の時には「100」という値が取得されます。
333行目「Me.Label11.Caption = ZoomRatio & "%"」では、その表示倍率に「%」をくっつけて、文字列としてLabel11に書き込んでいます

7ー4ー2ー4.測定値表示
ユーザーフォームの位置やサイズに変更があった場合に発生するのがLayoutイベントです。
  1. '========== ⇩(15) 測定値表示 ============
  2. Private Sub UserForm_Layout()
  3.  Dim X As Single     '←X方向のポイント・ピクセル
  4.  Dim Y As Single     '←Y方向のポイント・ピクセル
  5.  Dim Pane2 As Range   '←左上領域の1番目セル
  6.  Dim Pane3 As Range   '←非可視領域の1番目セル
  7.  Dim R As Object     '←フォームの位置にあるセル又はオブジェクト
  8.  Call Zoom_Ini
  9.  X = Me.Left + (Me.Width - Me.InsideWidth) / 2
  10.  Y = Me.Top
  11.  Me.Label1.Caption = Format(X, "#0.00")
  12.  Me.Label2.Caption = Format(Y, "#0.00")
  13.  Me.Label3.Caption = Format(X * (96 / 72) * WinScreenRatio, "#0.00")
  14.  Me.Label4.Caption = Format(Y * (96 / 72) * WinScreenRatio, "#0.00")
  15.  With ActiveWindow
  16.   X = X - .Panes(1).PointsToScreenPixelsX(0) / (96 / 72) / WinScreenRatio
  17.   Y = Y - .Panes(1).PointsToScreenPixelsY(0) / (96 / 72) / WinScreenRatio
  18.   X = X / (.Zoom / 100)
  19.   Y = Y / (.Zoom / 100)
  20.   Set Pane2 = .Panes(.Panes.Count).VisibleRange.Cells(1)
  21.   Set Pane3 = .Panes(1).VisibleRange.Cells(1).Offset(.SplitRow, .SplitColumn)
  22.   If X > Pane3.Left Then
  23.    X = X + (Pane2.Left - Pane3.Left)
  24.   End If
  25.   If Y > Pane3.Top Then
  26.    Y = Y + (Pane2.Top - Pane3.Top)
  27.   End If
  28.  End With
  29.  Me.Label5.Caption = Format(X, "#0.00")
  30.  Me.Label6.Caption = Format(Y, "#0.00")
  31.  X = X * (96 / 72) * WinScreenRatio
  32.  Y = Y * (96 / 72) * WinScreenRatio
  33.  Me.Label7.Caption = Format(X, "#0.00")
  34.  Me.Label8.Caption = Format(Y, "#0.00")
  35.  Set R = ActiveWindow.RangeFromPoint(Val(Me.Label3.Caption), Val(Me.Label4.Caption))
  36.  If R Is Nothing Then
  37.   Me.Label9.Caption = "Nothing"
  38.  ElseIf TypeName(R) = "Range" Then
  39.   Me.Label9.Caption = R.Address
  40.  Else
  41.   Me.Label9.Caption = R.Name
  42.  End If
  43.  Set Pane2 = Nothing
  44.  Set Pane3 = Nothing
  45.  Set R = Nothing
  46. End Sub
図46


348行目「Call Zoom_Ini」では図45を呼出し、現状の表示倍率の取得と表示を行います。

次に、350行目「X = Me.Left + (Me.Width - Me.InsideWidth) / 2」でユーザーフォームのX方向、351行目「Y = Me.Top」でY方向の位置を取得しています。
今回はユーザーフォームの左上角を基準点として測定をしていますので、350行目は「X = Me.Left」では?と思われたかもしれません。それでも良いのですが、実際のLeft位置と見た目のフォームの左位置には、図47のようにズレがあります。
フォームの見掛けの幅と本当の幅
図47


図47は、ユーザーフォームのLeft位置、Top位置をJ2セル左上に合わせたものです。上端はJ2セル上端に合っているようですが、左端にはスキマが空いています。見た目のフォームの幅はInsideWidthで得られますので、今回は「(Me.Width - Me.InsideWidth) / 2」を補正し、「見た目のフォーム左上角」を基準とすることにしました。
なお、350~351行目で受け取った値は、ポイント単位です。

353行目「Me.Label1.Caption = Format(X, "#0.00")」、354行目「Me.Label2.Caption = Format(Y, "#0.00")」では、「スクリーン座標 × ポイント単位」のLabelに値を表示させています。なお、小数点以下の値が測定結果によりばらつく可能性がありますので、Format関数で小数点以下2桁表示に揃えています。

Label3~Label4は「スクリーン座標 × ピクセル単位」です。図26では左上のエリアに相当します。
元のデータは図26の左下エリアですので、「ディスプレイ拡大率 × (96/72)」を掛ければ良いことになります。
355行目「Me.Label3.Caption = Format(X * (96 / 72) * WinScreenRatio, "#0.00")」では、350行目で取得したフォームの左位置に対して「ディスプレイ拡大率 × (96/72)」を掛け、その値を小数点以下2桁に揃えています。
Y方向については、356行目「Me.Label4.Caption = Format(Y * (96 / 72) * WinScreenRatio, "#0.00")」で同様に換算をします。

358行目「With ActiveWindow」では、359~374行目を現在のシート基準とします。
359行目「X = X - .Panes(1).PointsToScreenPixelsX(0) / (96 / 72) / WinScreenRatio」、360行目「Y = Y - .Panes(1).PointsToScreenPixelsY(0) / (96 / 72) / WinScreenRatio」では、図26の左下エリアで「ドキュメント座標に移動」するための準備としてドキュメント座標原点を引算します。その際、図26でExcel原点を左下エリアに持ってくるために、Excel原点を「ディスプレイ拡大率 × (96/72)」で割り算しています。

362行目「X = X / (.Zoom / 100)」、363行目「Y = Y / (.Zoom / 100)」では、左下エリアから右下エリアへ移動し、その際に「表示倍率」を割り算します。

ドキュメント座標に入ったら、非可視領域の距離を足し算します。
まず365行目「Set Pane2 = .Panes(.Panes.Count).VisibleRange.Cells(1)」で左上領域の1番目セルを取得し、366行目「Set Pane3 = .Panes(1).VisibleRange.Cells(1).Offset(.SplitRow, .SplitColumn)」で非可視領域の1番目セルを取得します。

368行目「If X > Pane3.Left Then」では、ユーザーフォームが分割線を越えているか否かを調べ、越えている場合は369行目「X = X + (Pane2.Left - Pane3.Left)」で、非可視領域の距離(幅)を足し算します。
372~374行目ではY方向の補正をします。

「ドキュメント座標 × ポイント単位」の補正が完了しましたので、378行目「Me.Label5.Caption = Format(X, "#0.00")」、379行目「Me.Label6.Caption = Format(Y, "#0.00")」でLabel5・Label6に値を書き込みます。

この時点でのX値・Y値の単位はポイントですので、381行目「X = X * (96 / 72) * WinScreenRatio」、382行目「Y = Y * (96 / 72) * WinScreenRatio」のように、ピクセルにするために「ディスプレイ拡大率 × (96/72)」を掛けます。
そしてその値を384行目「Me.Label7.Caption = Format(X, "#0.00")」、385行目「Me.Label8.Caption = Format(Y, "#0.00")」で、Label7・Label8に表示させています。

387~395行目は、フォームの位置が指している「セルまたはオブジェクト名」を表示するコードです。これを実現するために「ActiveWindow.RangeFromPoint」メソッドを使い、引数に「スクリーン座標のピクセル値(第一引数=X方向、第二引数=Y方向)」を指定します。
この「スクリーン座標のピクセル値」はLabel3とLabel4で既に表示されているため、その値(文字列)をVal関数で数値に変換し、387行目「Set R = ActiveWindow.RangeFromPoint(Val(Me.Label3.Caption), Val(Me.Label4.Caption))」のように引数指定をし、変数Rでオブジェクトを受け取ります。

RangeFromPointメソッドは、セル又は図形等のオブジェクトを戻します。それ以外(≒シートの可視範囲を外れた。また分割線上にある)の時にはNothingを戻します。なおオブジェクトが存在する場合は、セルよりもオブジェクトが優先するようです。

但しウィンドウ分割・ウィンドウ枠固定をすると「最も遠い領域(ActiveWindow.Panes(ActiveWindow.Panes.Count)」が全ての可視領域を占めている と認識しているようです。
ですので図48のように、最遠領域上で見てオブジェクトであればオブジェクトが戻ります。オブジェクトでは無い時はRangeオブジェクトが戻るのですが、その際は何故か「正しいセル」が戻ります(図48の一番右側)。
分割すると最終領域上でのオブジェクトが戻る
図48


この矛盾を解消するには、分割状態の時には領域ごとに表示範囲を把握し、ユーザーフォーム位置から最遠領域の位置を補正してやれば正しいオブジェクトが表示される気はするのですが、結構面倒そうなので今回は諦めました。RangeFromPointメソッドを使う場合は注意が必要です。

ウィンドウ分割時等は正しいオブジェクトが戻らない場合がある事を前提に、セルのアドレスやオブジェクト名を表示させるのが389~395行目です。
まず389行目「If R Is Nothing Then」で、戻り値がNothingだった時を選り分けます。その際は390行目「Me.Label9.Caption = "Nothing"」で"Nothing"と表示することにしました。
Nothing以外の時はRangeや図形等のオブジェクトです。オブジェクトの型を表示するのも1つの方法ですが、今回はセルの場合はアドレスを、それ以外のオブジェクトの場合はオブジェクトの名前を表示させることにしました。
セルの場合は391行目「ElseIf TypeName(R) = "Range" Then」で選り分け、392行目「Me.Label9.Caption = R.Address」でセルのアドレスを表示させます。
それ以外(393行目「Else」)の時は図形などのオブジェクトですので、394行目「Me.Label9.Caption = R.Name」でオブジェクトの名前("図形01" など)を表示させます。

なお、Layoutイベントは親コンテナの位置に変更があっても発生するのですが、ユーザーフォームの親相当のExcel本体の移動があってもLayoutイベントは発生しません。確かにユーザーフォームのプロパティを探してもParentがありません。
ですので、フォームを起動したままExcel本体を移動しても値が変化せず、間違った表示値となることに御注意下さい。

8.まとめ

今回は「スクリーン座標とドキュメント座標」「ピクセル単位とポイント単位」の変換に加え、「ウィンドウ枠固定やウィンドウ分割」の補正について説明してきました。これらの変換式を作成する際には、私も昔から「こうだっけ?」「ああだっけ?」と毎回悩んでいたので、今回は分かり易い説明図を という気持ちで作ったのが図26です。
「Excel原点」を補正する部分には、まだ改良の余地がありそうですが、これからはあまり悩まなくても済みそうな気がします。

但し換算時の誤差は、ディスプレイ拡大率や表示倍率が100%以外の時には特に大きい様です。どのような条件でどのくらい誤差が発生するかは今回調べませんでしたが、分かったとしても誤差を補正できるかも分かりません。
「だいたい位置が合っている」くらいの意識で使うしか無いように思われます。

アプリ実例・関連する項目

Excel図形等の位置、ディスプレイ上の位置の取得
フォームコントロールの内側サイズ

サンプルファイル

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