2023/02/06

フォームコントロールの内側サイズ




1.概要

ユーザーフォーム上に配置するコントロールの内、「Frame」と MultiPageの「Page」部分は操作するコントロールでは無く、他のコントロールの「ベース」となるものです。またUserForm自体もコントロールのベースとなっています。
これら「UserForm」「Frame」「Page」には、他のコントロールには無いプロパティがあります。

全てのコントロールには、その寸法を取得/設定する「Width」「Heitht」というプロパティがあるのですが、「UserForm」「Frame」「Page」にはベースの役割をする上で必要な「InsideWidth」「InsideHeight」プロパティがあるのです。
今回は、この「InsideWidth」「InsideHeight」プロパティに注目します。

2.ユーザーフォーム(UserForm)

2ー1.WidthとHeight

まず「Width」と「Height」ですが、フォームの見た目の幅・高さでは無いようです。
例えば図1のような設定でユーザーフォームを起動すると、フォームの左上角が「Excelのドキュメント座標原点」に一致します。
  1. '========== ⇩(1) フォーム設定(左上角を原点に合わせる) ============
  2. Private Sub UserForm_Initialize()
  3.  Me.StartUpPosition = 0
  4.  Me.Left = ActiveWindow.PointsToScreenPixelsX(0) * 0.75
  5.  Me.Top = ActiveWindow.PointsToScreenPixelsY(0) * 0.75
  6. End Sub
図1


02行目「Me.StartUpPosition = 0」で、フォームの起動位置を手動モードにします。
03行目「Me.Left = ActiveWindow.PointsToScreenPixelsX(0) * 0.75」では、フォームの左端の位置をExcelドキュメント座標原点(X:左右方向)に合わせます。なお「PointsToScreenPixelsX(0)」の単位はピクセルですので、フォームの位置指定に使用するポイント単位に変換するため「0.75」を掛け算します。なおこれは、Excelの表示倍率が100%の時です。
04行目「Me.Top = ActiveWindow.PointsToScreenPixelsY(0) * 0.75」では、フォームの上端の位置をExcelドキュメント座標原点(Y:上下方向)に合わせます。

一方、図2のような設定でユーザーフォームを起動すると、フォーム右下角が「Excelのドキュメント座標原点」に一致します。
  1. '========== ⇩(2) フォーム設定(右下角を原点に合わせる) ============
  2. Private Sub UserForm_Initialize()
  3.  Me.StartUpPosition = 0
  4.  Me.Left = ActiveWindow.PointsToScreenPixelsX(0) * 0.75 - Me.Width
  5.  Me.Top = ActiveWindow.PointsToScreenPixelsY(0) * 0.75 - Me.Height
  6. End Sub
図2


12行目「Me.StartUpPosition = 0」は図1と同じく、フォームの起動位置を手動モードにしています。
13行目「Me.Left = ActiveWindow.PointsToScreenPixelsX(0) * 0.75 - Me.Width」は、図1の03行目に対して「フォームの幅」を差し引いていますので、フォームの右端が原点に合うことになります。
14行目「Me.Top = ActiveWindow.PointsToScreenPixelsY(0) * 0.75 - Me.Height」は、図1の04行目に対して「フォームの高さ」を差し引いていますので、フォームの下端が原点に合うことになります。
13~14行目を合わせて、フォームの右下角を原点に合わせています。

図1の設定でフォームを起動した結果が図3の左側、図2の設定で起動した結果が右側です。
フォームの幅と高さの検証
図3


図3でも分かるように、フォームの実際の左右端は見かけの左右端よりも少しだけ外側です。あたかも両端が透明になっているような形です。
また図3の右側では、フォームの下端も見かけより外側となっています。なお上端は、ほぼ見た目通りです。

2ー2.InsideWidthとInsideHeight

UserFormのInsideWidthとInsideHeightの範囲を調べる為に、まずユーザーフォーム内に「Imageコントロール」を配置します。そのImageをフォーム内部一杯に広げるのが図4のコードです。なお数多くあるコントロールの中からImageを選択した理由は、色々試してみて最も境界が明確だったからです。
  1. '========== ⇩(3) フォーム設定(フォーム内部一杯にコントロールを表示) ============
  2. Private Sub UserForm_Initialize()
  3.  Me.Image1.Left = 0
  4.  Me.Image1.Top = 0
  5.  Me.Image1.Width = Me.InsideWidth
  6.  Me.Image1.Height = Me.InsideHeight
  7.  Me.Image1.BorderColor = RGB(255, 0, 0)
  8. End Sub
図4


22行目「Me.Image1.Left = 0」で、Imageコントロールの左端をフォーム内側左端に合わせます。
23行目「Me.Image1.Top = 0」で、Imageコントロールの上端をフォーム内側上端に合わせます。
24行目「Me.Image1.Width = Me.InsideWidth」で、Imageコントロールの横幅を内側一杯に延ばします。
25行目「Me.Image1.Height = Me.InsideHeight」で、Imageコントロールの高さを内側一杯に延ばします。
26行目「Me.Image1.BorderColor = RGB(255, 0, 0)」で、Imageコントロールの外枠線を赤色にします。

図4のコードでフォームを起動すると、図5のようになります。
赤線(Imageコントロールの外枠線)で示されている範囲が、フォームのInsideWidth × InsideHeight になります。
フォーム内側のサイズ検証
図5


図5のフォームの寸法を調べてみると、以下のようになりました。
項目寸法(ポイント)
Width216.75
InsideWidth204.75

Height189
InsideHeight159.75
図6


もちろん絶対値はどうでも良いのですが、内側と外側の「差」はフォームのサイズに関係無く一定のようです。但し、フォームのプロパティ(例えばBorderStyle)によっては、内側寸法が変化します。

また、この値を検証するために図7のように「セルのサイズ」で測定してみました。
フォーム内側のサイズ検証
図7


フォーム内側の赤枠に沿うようにセル幅・高さを合わせ、マウスで列・行の境界線をクリックすると図7の緑字のように寸法が表示されます。幅の表示値は「文字数」ですが、カッコ内にピクセル単位での値が表示されますし、高さは「ポイント数」とカッコ内にピクセル値が表示されます。

ピクセル値は「ディスプレイ拡大率=100%」「Excel表示倍率=100%」であれば、 ピクセル値 × 0.75 = ポイント値 ですので、計算すると図6の値に一致することが分かります。

寄り道
図7とは別な方法での検証も考えてみました。図8のようにフォームレイアウト画面でのグリッドの数を数える方法です。
フォームレイアウト画面でのサイズ検証
図8


グリッドの間隔を数えてみると、ちょうど34個です。これに、VBEの「ツール」→「オプション」→「全般」で設定可能な「グリッドの表示幅」を掛けると「34 × 6 = 204」となります。「グリッドの表示幅」の単位は「twip」なので「20 twip = 1ポイント」で計算をして「204 ÷ 20 = 10.2」・・・

と、ここまで計算して「なに?10.2ポイント!?」と気が付きました。あまりにも小さい値だからです。
そこで、10.2ポイントに近い「10ポイントのフォント」でフォーム上に文字列を配置してみたのが図9です。
フォーム上に10ポイントで文字を書いてサイズを検証
図9


やはり横幅は「10.2ポイント」では無いように思えます。そこで見返してみると「204twip ≒ 図6の204.75」に気づきます。
と言うことは、VBEのグリッド間隔は 「×:twip単位 〇:ポイント単位」という事になります。

グリッド間隔の単位「twip」は、今まで気にもしていなかったのですが「誤記」の可能性があります。と言って、この単位を見ながらコントロールを配置している方は少ないと思うので、実害は無さそうです。

3.Frameコントロール

3ー1.WidthとHeight

フォーム上でのFrameコントロールのWidthとHeightを調べる為に、フォーム上にFrameを1つ配置し図10の設定をします。
  1. '========== ⇩(4) フォーム設定(フォーム内部一杯にFrameを表示) ============
  2. Private Sub UserForm_Initialize()
  3.  Me.Frame1.Left = 0
  4.  Me.Frame1.Top = 0
  5.  Me.Frame1.Width = Me.InsideWidth
  6.  Me.Frame1.Height = Me.InsideHeight
  7.  Me.Frame1.BorderStyle = fmBorderStyleSingle
  8.  Me.Frame1.BorderColor = RGB(255, 0, 0)
  9. End Sub
図10


32行目「Me.Frame1.Left = 0」で、Frameの左端をフォーム内側左端に合わせます。
33行目「Me.Frame1.Top = 0」で、Frameの上端をフォーム内側上端に合わせます。
34行目「Me.Frame1.Width = Me.InsideWidth」で、Frameの横幅をフォーム内側一杯に延ばします。
35行目「Me.Frame1.Height = Me.InsideHeight」で、Frameの高さをフォーム内側一杯に延ばします。
36行目「Me.Frame1.BorderStyle = fmBorderStyleSingle」で、Frameの枠線を「線あり」にします。
37行目「Me.Frame1.BorderColor = RGB(255, 0, 0)」で、Frameの枠線色を赤色にします。
なお36行目のBorderStyle設定をしない(既定は枠線無し)と、37行目で枠線色を設定しても着色しません。

フォームを起動した結果が図11の中央図です。
Frameの外側寸法
図11


Frameの左右端は「フォーム内側一杯」に設定される事が分かります。また下端もフォーム内側一杯です。
一方、上端は「Frameの文字列の上端」になっています。Frameの枠線は「文字列の中央辺り」の高さになります。

なお図11の中央図の元は、図11の右側図です。下でInsideWidthとInsideHeightを説明するために、Frameの中にImageコントロールを1つ配置しています。

3ー2.InsideWidthとInsideHeight

FrameのInsideWidthとInsideHeightを確認する為に、図11の右側図のようなフォームを作成し図12のような設定をします。
  1. '========== ⇩(5) フォーム設定(Frame内側にImageを一杯に表示) ============
  2. Private Sub UserForm_Initialize()
  3.  Me.Frame1.Left = 0
  4.  Me.Frame1.Top = 0
  5.  Me.Frame1.Width = Me.InsideWidth
  6.  Me.Frame1.Height = Me.InsideHeight
  7.  Me.Image1.Left = 0
  8.  Me.Image1.Top = 0
  9.  Me.Image1.Width = Me.Frame1.InsideWidth
  10.  Me.Image1.Height = Me.Frame1.InsideHeight
  11.  Me.Image1.BorderColor = RGB(255, 0, 0)
  12. End Sub
図12


52~55行目は図10の32~35行目と同じで、フォーム内一杯にFrameを表示させています。枠線に着色はしていません。

57行目「Me.Image1.Left = 0」で、Imageの左端を「Frame内部」の左端に合わせています。
寄り道
今回は図11の右側図のように「Frame1の内部にImage1コントロールを配置」しています。このように配置すると、Image1はFrame1のコントロール配下になる(Frame1のControlプロパティにImage1が登録される)ので、Image1の位置は「Frame1ベース」という事になります。

また、Frame内に配置されたImageのParentを調べる(Me.Image1.Parent)事で、「Frame1」オブジェクトが親だと分かります。もしFrame外にImageを置いた場合は、Parentを調べると「UserForm」オブジェクトが親となります。

子供にとっての親(Parent)は「コンテナ」と呼ばれるので、Frameの配下になる事は「コンテナ化」する と言っても良いかもしれません。

58行目「Me.Image1.Top = 0」で、Imageの上端を「Frame内部」の上端に合わせています。
59行目「Me.Image1.Width = Me.Frame1.InsideWidth」で、Imageの横幅をFrame内部一杯に延ばします。
60行目「Me.Image1.Height = Me.Frame1.InsideHeight」で、Imageの高さをFrame内部一杯に延ばします。
61行目「Me.Image1.BorderColor = RGB(255, 0, 0)」で、Image外枠線の色を赤色に設定します。

起動したフォームが図13です。(一番左側の図は、フォームの内側領域を明示しているものです。)
Frameの内側寸法
図13


図13の中央と右側では、FrameコントロールのFontサイズが異なります。中央が(標準の)9ポイント、右側が18ポイントに設定変更したものです。
Frame枠線(薄い灰色)に沿った赤い枠線が、Frame内側の領域(=Imageの外側)です。その枠線は、FrameのFontサイズによって違ってきます。サイズが大きくなる(図13の右側図)と、枠線が下がってきます。

また「Frameの文字列のところで、赤いImageの枠線が切れている」ことも確認できます。切れていると言うことは「Frame内に配置したコントロールの一部が見えない」事になります。
そこで「Frame内側の上部から、何ポイント下げればコントロールが隠れないか」を調べたのが図14です。
Frame内部のコントロールが隠れない限界
図14


ここではFontサイズのみを変えていますが、他のプロパティ(例えばBorder)でも影響を受けますので御注意下さい。
大雑把な考え方では、「Fontサイズの半分(例えばFontが9ポイントであれば4.5ポイント、Fontが18ポイントであれば9ポイント)以上」を下げれば、Frame内のコントロールが隠れることは無さそうです。

4.MultiPageコントロール

ここで扱うMultiPageコントロールは、複数のPageの集合体です。MultiPage内の1つ1つのページが「Page」となります。
以下で説明する「WidthとHeight」はMultiPageのプロパティであり、また「InsideWidthとInsideHeight」はPageのプロパティです。しかし、MultiPageには「InsideWidthとInsideHeight」は無く、Pageには「WidthとHeight」はありません。

4ー1.WidthとHeight

MultiPageコントロールのWidthとHeightを調べるために、フォーム上にMultiPageを1つ配置し、図15の設定をします。
  1. '========== ⇩(6) フォーム設定(フォーム内部一杯にMultiPageを表示) ============
  2. Private Sub UserForm_Initialize()
  3.  Me.MultiPage1.Left = 0
  4.  Me.MultiPage1.Top = 0
  5.  Me.MultiPage1.Width = Me.InsideWidth
  6.  Me.MultiPage1.Height = Me.InsideHeight
  7.  Me.MultiPage1.BackColor = RGB(200, 200, 200)
  8. End Sub
図15


72行目「Me.MultiPage1.Left = 0」で、MultiPageの左端をフォーム内側左端に合わせます。
73行目「Me.MultiPage1.Top = 0」で、MultiPageの上端をフォーム内側上端に合わせます。
74行目「Me.MultiPage1.Width = Me.InsideWidth」で、MultiPageの横幅をフォーム内側一杯に延ばします。
75行目「Me.MultiPage1.Height = Me.InsideHeight」で、MultiPageの高さをフォーム内側一杯に延ばします。
76行目「Me.MultiPage1.BackColor = RGB(200, 200, 200)」で、MultiPageの背景色を「灰色」にします。背景色を変更したのは、MultiPageには枠線というものが無い為の代用です。

フォームを起動した結果が図16の中央図です。
MultiPageの各寸法
図16


MultiPageの左右は「フォーム内側一杯」に設定される事が分かります。
一方、上下方向の下端はフォーム内側一杯で、上端は「MultiPageのタブ上端」となっています。

なお図16の中央図の元は、図16の右側図です。下でInsideWidthとInsideHeightを説明するために、MultiPageの1ページ目の中にImageコントロールを1つ配置しています。

4ー2.InsideWidthとInsideHeight

PageのInsideWidthとInsideHeightを確認する為に、図16の右側図のようなフォームを作成し、図17の設定をします。
  1. '========== ⇩(7) フォーム設定(MultiPage内部一杯にImageを表示) ============
  2. Private Sub UserForm_Initialize()
  3.  Me.MultiPage1.Left = 0
  4.  Me.MultiPage1.Top = 0
  5.  Me.MultiPage1.Width = Me.InsideWidth
  6.  Me.MultiPage1.Height = Me.InsideHeight
  7.  DoEvents: DoEvents
  8.  Me.Image1.Left = 0
  9.  Me.Image1.Top = 0
  10.  Me.Image1.Width = Me.MultiPage1.Pages(0).InsideWidth
  11.  Me.Image1.Height = Me.MultiPage1.Pages(0).InsideHeight
  12.  Me.Image1.BorderColor = RGB(255, 0, 0)
  13. End Sub
図17


92~95行目は、図15の72~75行目と同じで、MultiPageをフォーム内側一杯に表示しています。

97行目「DoEvents: DoEvents」では制御を一旦O/Sへ戻し、「Pageの内側寸法の変更内容をフォームに反映」させています。
寄り道
この「DoEvents」が担っているのは「変更内容の反映」という機能です。
普通に考えれば94~95行目の実行と共に「内側寸法が変更」されるはずなのですが、試行してみるとDoEventsを実行する直前では「元サイズのPageの内側寸法」を保持しているのです(PC環境により異なるかもしれません)。そのため、ここでDoEventsを実行しないと、101~102行目で変更設定するImageコントロールのサイズが旧データ(最大には広がらない)となってしまいます。

しかし、Frame内にImageを配置した図12の場合には「DoEventsが不要」でしたので、何か理由がありそうです。例えばFrameにはWidthとInsideWidthが両方存在するのに対し、MultiPageにはWidthがあるがInsideWidthは無く、子どもに相当する「Pageに存在」する等です。
また「コンボボックスのテキストボックス部に複数列を表示」でも、DoEventsを使ってListBoxのサイズを反映させる必要がありましたが、何か今回と状況が似ているような気もします。

また図16の右図のように、1ページ目(=Me.MultiPage1.Pages(0) )を開いた形でMultiPageを配置すると、DoEventsを実行した時に正しい値になるのは1ページ目だけです。
且つ、途中で「MultiPage1.Value = 1」等を実行して「ページを切り替える」と、切り替えられたページも正しい値に切り替わります。切り替えられていないページ(= 一度も表面に出てきていないページ)は古い値のままなのです。

結局、原因は分からないのですが、ActiveX(ユーザーフォーム)のコントロールでサイズ変更が反映されない状況が発生したら「DoEventsを入れてみる」というのが、1つの解決法のようです。

99行目「Me.Image1.Left = 0」で、Imageの左端を「Page内部」の左端に合わせています。
100行目「Me.Image1.Top = 0」で、Imageの上端を「Page内部」の上端に合わせています。
101行目「Me.Image1.Width = Me.MultiPage1.Pages(0).InsideWidth」で、Imageの横幅をPage内部一杯に延ばします。ここでInsideWidthを取得するページは「配置時に表面に出ているページ」または「途中で表面にしたページ」にする必要がありますので、1ページ目( Pages(0) )を指定しています。
102行目「Me.Image1.Height = Me.MultiPage1.Pages(0).InsideHeig」で、Imageの高さをPage内部一杯に延ばします。
103行目「Me.Image1.BorderColor = RGB(255, 0, 0)」で、Image外枠線の色を赤色に設定します。

起動したフォームが図18です。(一番左側の図は、フォームの内側領域を明示しているものです。)
MultiPageの各寸法
図18


図18の中央と右側では、PageのFontサイズが異なっています。中央が(標準の)9ポイント、右側が18ポイントに設定変更したものです。
赤い枠線で囲まれ「Page1」「Page2」・・・のタブの下側が、Page内側の領域(=Imageの外側)です。Frameコントロールよりは分かり易いですし、タブで領域が隠れてしまうこともありません。
なおFontサイズにより内側寸法が変わるのは同じで、サイズが大きくなる(図18の右側図)と、内側領域も小さくなります。

5.まとめ

UserForm/Frame/MultiPage(Page)の「Width」「Heitht」及び「InsideWidth」「InsideHeight」の寸法は、図19のようになります。青色が外側寸法(WidthとHeitht)、緑色が内側寸法(InsideWidthとInsideHeight)です。
UserForm・Frame・MultiPageのサイズ
図19


今回説明した内部寸法(InsideWidth、InsideHeight)は、そのコントロールのサイズ(Width、Heitht)が同じでも、Fontサイズや他プロパティにより値が変わってきます。また「InsideWidth」「InsideHeight」は、取得のみが可能なプロパティなので、必要なサイズを直接指定する訳にもいきません。
最終的には手調整しながらコントロールを配置するしか無いのでしょうが、今回の説明内容が少しでも参考になれば幸いです。

アプリ実例

MonthViewコントロールを使ったカレンダー
Excel図形等の位置、ディスプレイ上の位置の取得
Book内で完結するフォーム表示のHelp画面
ラベルカレンダーをクリックし日付入力
年賀状リスト等の宛名検索(ブック内検索可)