2022/03/22

ボタンを自動生成するフォームカレンダー




1.背景

以前「セルへの日付入力をカレンダー日付クリックで選定する」の項で、ボタン式のフォームカレンダーを紹介しました。その時は、カレンダーの日付に使用する6行×7列=42個のCommandButtonをフォーム上に並べるところからスタートしていたため「作る人にとっては、面倒だろうな」と、ずっと思っていました。
そこで今回は、「ボタンはマクロ側で生成する」カレンダーを紹介します。特徴としては、フォームのデザイン時に苦労せずに済むのと同時に、ボタンのサイズを自由に変更できますのでユーザビリティも向上すると思います。

但しクラスの役目として、前回は「存在するボタンのClickイベントを拾う」だけで良かったのですが、今回はそれに「マクロで作ったボタンに文字を書き込んだり、色を付けたりする」ことも追加されるため、少しだけ複雑になっています。

2.システム概要

図2-1のように、ワークシート上に2ヶ所の日付入力セル(黄色いセル)があるとします。そのセルに対し、図4-2のようなSelectionChangeイベントを仕掛けておき、そのセルを選択したらフォームカレンダーが表示され、選択した日付をそのセルに書き込むのが今回のシステムです。
セル値による表示カレンダーの違い
図2-1

日付入力セルが「空白セル」や「日付では無い文字列」が入っている場合、そのセルを選択①すると「今日の日付が黄色」で示された「今月のカレンダー②」が表示されます(図2-1の左側)。
一方、日付が入ってるセルを選択③した場合は、「その日付が黄色」で示された「その日を含む月のカレンダー④」が表示されます(図2-1の右側)。

フォームカレンダーは、上部中央に「表示カレンダーの年月⑤」が表示されており、左上に配置したSpinButton⑥を操作することでカレンダーを1ヶ月ずつ前後することが出来ます。
希望の年月にカレンダーを移動させたら、希望の日付のボタンをクリック⑦することで、「カレンダーが消え」ると同時に「選択したセルに日付が入力」されます。
フォームカレンダーの各機能説明
図2-2

カレンダーの日付は、日曜~土曜の順番で並べており、日曜はピンク色、土曜は水色⑩で表示されます。選択したセルの値が「空白セル」または「日付では無い文字列」である場合は、本日の日付が黄色になります。一方、選択したセルの値が「日付」であった場合は、その日付が黄色になります。
また、フォーム上の右上にある「小」又は「大」と表示されているボタン⑨(ToggleButton)をクリックすると、カレンダーの表示サイズが大⇔小に切り替わります。

フォームカレンダー上の日付ボタンをクリックすることでカレンダーは消えますが、カレンダーが表示されている時に「日付入力をキャンセル=カレンダーを終了⑧」する場合は、「ESCキー」を押すか「フォーム上の右上×印」をクリックします。
終了させた場合は、選択したセル値は元のまま(=元の値を書き込み)となります。
なお「ESCキーで終了」させた時は「フォームをHideで隠した」状態にしていますので、次回入力セルを選択した際には「フォームサイズを変更した状態を保ったまま」起動することになります。逆に「右上×印で終了」させると「フォームを削除(UnLoad)」することになりますので、次回起動時は初期状態(フォームサイズ=小(初期値))で表示されます。

なお本システムをExcelにアドイン登録しておくと、日付を入力するワークシートのシートモジュールにイベントプロシージャを記述することで、フォームカレンダーを使用することが出来ます。

3.プログラムの流れ

今回サンプルファイルでは、日付を入力するセルにはSelectionChangeイベントが仕掛けてあり、そのセルをセル選択することでカレンダー作成を開始します。カレンダー作成は、1つ1つ「カレンダーのどの位置にどの日付を配置するか」を計算し、その位置に「ボタンを作成」します。ボタンを1つ作成するたびに、クリックした時に反応できるようにボタンをClickイベントに登録していきます。1ヶ月分のボタン作成が終了したら、フォームを表示します。
プログラムの流れ
図3-1

ユーザーがフォームカレンダーの年月を移動させるためにスピンボタンを操作すると、新たに表示するカレンダーの年月を計算し、また「カレンダーのボタンの位置を計算しながら、ボタン作成→ボタンのClickイベントへの登録」をし直します。
また、フォームのサイズを変更するトグルボタンを操作すると、ボタンサイズの設定値を変更した上で、また「カレンダーのボタンの位置を計算しながら、ボタン作成→ボタンのClickイベントへの登録」を行います。

ユーザーが、カレンダー上のボタンをクリックすると、そのボタンの数字(=日付)とカレンダー表示年月を組み合わせて「クリックした日付」を計算し、SelectionChangeイベントへ戻します。SelectionChange側では、戻された値(=クリックした日付)を日付入力セルに書き込みます。

なお、ユーザーが日付をクリックせずにカレンダーを終了させた時には、セル選択した時に保存しておいた「元の値」をそのままセルに書き込みます。

4.入力用シート(サンプルファイルではSheet1)

4-1.ワークシート

ワークシート上の数式や加工などは、本システムでは不要です。必要なのは、SelectionChangeイベント等の設定です。
なおサンプルファイルでは、入力セルがどこなのか分かるように、図4-1のように黄色背景にしています。
ワークシート上は加工不要
図4-1

4-2.シートモジュール

入力シートには図4-2のような「Worksheet_SelectionChange」イベントプロシージャを置きます。そして設定した日付入力セルを選択した時に、カレンダーを呼び出します。
  1. '========== ⇩(1) 入力シートのSelectionChangeイベント ============
  2. Private Sub Worksheet_SelectionChange(ByVal Target As Range)
  3.  If Target(1).Address = Range("B2").Address Or _
  4.   Target(1).Address = Range("D2").Address Then
  5.   Target(1).Value = Workbooks("it-078.xlam").Application.Run("CalSelect", Target(1))
  6.   'Target(1).Value = CalSelect(Target)   '←同一ブック内にカレンダーマクロがある場合
  7.  End If
  8. End Sub
図4-2

3~4行目のIf文で、日付入力セルに入った時だけ6~7行目を実行させるようにしています。
3行目「If Target(1).Address = Range("B2").Address Or _ 」は、選択されたセル(の左上の単一セル)がB2セルの場合で、4行目「Target(1).Address = Range("D2").Address Then」はD2セルの場合です。
TargetとRange()のRange型を直接比較してしまうと「セルに書かれている値」を比較してしまいますのでNGです。そのため「.Address」で「セルの位置」として比較をしています。

6行目「Target(1).Value = Workbooks("it-078.xlam").Application.Run("CalSelect", Target(1))」は、本システムをExcelにアドイン(it-078.xlsm → it-078.xlam)した場合の「フォームカレンダーを呼び出すコード」です。
この場合、システム起動プロシージャはアドイン登録した「it-078.xlam」の「CalSelectプロシージャ(標準モジュール内)」となります。また引数は、選択したセル範囲「Target(1)」を渡します。
アドインファイル上のプロシージャは直接呼び出せませんので、Runメソッドを使用しカッコ内にプロシージャ名と引数をカンマで区切って渡すことになります。「CalSelectプロシージャ」からは、日付入力セルに書き込む値が戻ってきますので、左辺は「Target(1).Value」と書き込むセルの値を指示します。
なお左辺も右辺も「Target(1)」としているのは、選択したセル範囲の左上1番目のセルに絞っている為です。「(1)」を付けないと、選択したセル範囲全てに日付が記入されることになります。

一方、見え消しにしてある7行目「Target(1).Value = CalSelect(Target)」は、システムが日付入力するブック内に存在する場合です。その場合は、標準モジュール上に置いてある「CalSelectプロシージャ」を呼び出すことになります。
なお「サンプルファイル」では、6行目を無効にし7行目を有効にしています。

5.標準モジュール(Module1)

標準モジュール先頭部分(宣言部:図5-1)では、システム内で共通して使用する変数の宣言をしています。
  1. '========== ⇩(2) システム内で使用する共有変数の宣言 ============
  2. Public setDay As Date      '←基準日付
  3. Public dispDay As Date     '←カレンダー表示年月
  4. Public OrgValue As Variant   '←元データ・戻しデータ
図5-1

13行目「Public setDay As Date」は、カレンダー内で「黄色背景にする日付」です。入力セルに日付が入っていれば、その日付が代入されますが、日付以外(空白を含む)の場合は、今日の日付を代入します。
14行目「Public dispDay As Date」は、表示する月単位カレンダーの初日の日付が代入されます。
15行目「Public OrgValue As Variant」は、日付入力セルに元々入っていた値が代入されます。ユーザーがカレンダーの日付ボタンをクリックした場合は、そのクリックした日付で置き換えられ、入力セル側に戻されます。またカレンダーの日付をクリックしなければ(=カレンダーの右上×印をクリック、またはESCキーで終了)、元々の値がそのまま戻ることになります。

この3つのPublic変数は、一見するとフォーム内変数でも良さそうですが、今回システムではクラス側で「カレンダーのボタンをクリック」したことを受け取ります。フォームとクラスの間でデータをやり取りするのは面倒そうなので、標準モジュールでPublic宣言をし、システム内で共有した形にしています。

5-1.起動プロシージャ

図5-2が「ワークシート側から呼び出される関数プロシージャ」で、引数として入力セルのセル範囲(Range型)を受け取ります。関数としての戻り値は、カレンダーの日付をクリックした場合は「その日付」を戻し、日付をクリックせずに終了させたら「元の値」を戻します。
  1. '========== ⇩(3) システム起動・値返却 ============
  2. Public Function CalSelect(Target As Range) As Variant
  3.  OrgValue = Target(1).Value
  4.  If IsDate(OrgValue) = False Then
  5.   setDay = Date
  6.  Else
  7.   setDay = CDate(OrgValue)
  8.  End If
  9.  dispDay = DateAdd("d", -1 * Day(setDay) + 1, setDay)
  10.  UserForm1.Show
  11.  CalSelect = OrgValue
  12. End Function
図5-2

19行目「OrgValue = Target(1).Value」は、受け取ったセル範囲の左上単一セル(=1番目のセル)の値(日付入力セルに元々入っていた値)を、変数OrgValueに代入します。

21~25行目は、その元々の値が日付か否かを調べ、変数setDayに「基準とする日付」の値を代入しています。この日付はカレンダー上では黄色背景のボタンになります。
21行目「If IsDate(OrgValue) = False Then」は、19行目で値が代入された変数OrgValue値(=選択したセルの値)が「日付型」かを調べます。日付では無い(= False)場合には、22行目の「setDay = Date」で「今日の日付」を変数setDayに代入します。つまり「今日の日付が基準日付」となります。
一方、選択したセルの値が日付だった(= True)場合には、24行目「setDay = CDate(OrgValue)」で、「セルの日付が基準日付」になります。

27行目「dispDay = DateAdd("d", -1 * Day(setDay) + 1, setDay)」は、まず表示するカレンダーの年月の初日を計算し、変数dispDayに設定します。
「年月の初日」の求め方は色々考えられますが、今回はDateAdd関数を使い「基準日付(setDay)に対して、その日付分だけ前の日」という計算で初日の日付を求めています。
つまり、選択セルの値が日付では無い場合は、まず今月のカレンダーが表示(+今日の日付は黄色背景で表示)され、選択セルの値が日付の場合は、その日付を含む年月のカレンダーが表示(+セルの日付が黄色背景で表示)されることになります。

29行目「UserForm1.Show」では、フォームカレンダーを「モーダルで表示(カレンダーの操作しか出来ない)」します。
表示されたカレンダー内で、日付のボタンをクリックした時には図7-4の212行目で「クリックした日付を変数OrgValueに書込み」を行ったあと、フォームカレンダーを閉じます。
ですのでカレンダーが閉じられたあと、31行目「CalSelect = OrgValue」が実行されることで、「クリックした日付を関数CalSelectの戻り値にする」ことになります。
なお、フォームカレンダーの日付ボタンをクリックせずに、右上×印をクリックしたり、ESCキーを押したりしてカレンダーを閉じた時は、「変数OrgValueは、19行目で代入した元々の値のまま」ですので、選択セルの値は変わりません(正確に言うと「同じ値で書き換えが行われた」ことになる)。

なお29行目で、もしモードレス(カレンダーが表示されていても、シート操作が可能)で表示してしまうと、すぐにシート側のSelectionChangeイベントへ制御が戻ってしまい、フォームカレンダー上で選んだ日付は選択セルに書き込まれなくなってしまいます。

6.フォーム(UserForm1)

6-1.フォームデザイン

カレンダーの日付ボタン等はマクロ側から作成しますので、フォーム上への作成・配置は不要です。但し、「年月移動用スピンボタン」「年月を表示するラベル」「フォームサイズを変えるトグルボタン」は、あらかじめフォーム上部に配置します。
また「ESCキーを押した時にフォームを閉じる」ためのコマンドボタンを「カレンダーの枠外(カレンダーになった後でも見えない場所)」に配置しておきます(「終了ボタン」として、見える場所に配置してもOKです)。
カレンダーのフォームデザイン
図6-1

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

先頭の宣言部には、フォームモジュール内で共有する変数の宣言をします。
  1. '========== ⇩(4) 変数の宣言 ============
  2. Dim Barray() As New Class1    '←ボタンの配列を宣言・生成
  3. Dim side As Single        '←ボタンの一辺の長さ(ポイント)
  4. Dim cTop As Single        '←フォーム上端からカレンダーまでの距離
  5. Dim cLeft As Single        '←フォーム左端からカレンダーまでの距離
  6. Dim EventOff As Boolean     '←再帰呼び出し防止用のフラグ変数
図6-2

35行目「Dim Barray() As New Class1」は、日付ボタンの配列を生成(New句)しています。型は「Class1」となっているように、クラスモジュールで宣言しているコマンドボタン型となります。

36~38行目は、図6-3のように「タイトル行を含むカレンダーの位置とサイズ」を示す変数の宣言です。
「side」は、ボタンの一辺の長さを表します。また「cTop」は、フォーム(コントロールを置く範囲内)上端からカレンダーまでの縦方向距離を表し、「cLeft」はフォーム左端からの横方向距離を表します。単位はポイントになります。
ボタン等の位置の変数
図6-3

39行目「Dim EventOff As Boolean」は、カレンダーの表示年月を移動させるSpinButtonを操作した際、同じプロシージャを再帰呼び出しすることを防ぐための「フラグ変数」です。

6-2-1.起動時・表示時設定

図5-2の29行目で、ユーザーフォームを「Show」した時、最初に呼び出されるのが図6-4のInitializeイベントです。
  1. '========== ⇩(5) 起動時設定 ============
  2. Private Sub UserForm_Initialize()
  3.  cLeft = 10
  4.  cTop = Me.Label1.Top + Me.Label1.Height + cLeft
  5.  side = 20
  6.  Me.Label1.Font.Size = 12
  7.  Me.SpinButton1.Max = 1
  8.  Me.SpinButton1.Min = -1
  9.  Me.SpinButton1.Value = 0
  10.  Me.CommandButton1.Cancel = True
  11.  Me.ToggleButton1.Caption = "小"
  12.  Call FormSizeSet
  13. End Sub
図6-4

43~45行目では、図6-2の36~38行目で宣言した変数に「タイトル行を含むカレンダーの位置とサイズ」の値を代入します。
43行目「cLeft = 10」は、フォーム左端からカレンダーボタン類までの横方向距離を10(ポイント)としています。
44行目「cTop = Me.Label1.Top + Me.Label1.Height + cLeft」は、年月を表示するLabel1の下端から「cLeft」下がったところからカレンダーボタン類としています。
そして45行目「side = 20」で、カレンダーボタン類の1つのサイズを定めています。
カレンダーボタンの位置
図6-5

43~45行目の設定は、図6-5の赤字の部分です。そして後述するFormSizeSetプロシージャ(図6-18)で青字の部分の距離(カレンダーボタン類の右端~フォーム右端、下端~フォーム下端)を定めています。
カレンダーボタン類の端~フォームの端の距離を全て「cLeft」で揃えることで、「カレンダーがフォームの中央部」に見えることを狙っています。

47行目「Me.Label1.Font.Size = 12」では、年月表示のラベルのフォントサイズを設定します。

49~51行目では、年月移動用のスピンボタンの移動量を定めています。
49行目「Me.SpinButton1.Max = 1」で最大値を+1に、50行目「Me.SpinButton1.Min = -1」で最小値をー1に、そして51行目「Me.SpinButton1.Value = 0」で初期状態をゼロにしています。
このことで、スピンボタンのプラス側をクリックすれば「1ヶ月カレンダーが進み」、マイナス側をクリックすれば「1ヶ月戻る」ことになります。

53行目「Me.CommandButton1.Cancel = True」は、隠しているCommandButton1のCancelプロパティをTrueにすることで、「ESCキーを押したらCommandButton1をクリックした」事にすることができます。今回、図6-10のCommandButton1のClickイベントでは「フォームを閉じる」コードを実行させますので、「ESCキー = カレンダーを閉じる」ことになります。
55行目「Me.ToggleButton1.Caption = "小"」では、フォームサイズを変更するトグルボタンの表面文字を「小」にしています。トグルボタンは初期値はFalse(凸型=OFF状態)ですので、「False=小」としています。
(アプリによっては、クリックした後の状態を表面文字にしているものもありますが、ここでは現状を表す文字としました。)

57行目「Call FormSizeSet」は図6-18を呼び出し、ボタンを作成する前に「フォームをサイズ変更」しています。
このInitializeイベント内でこの「サイズ変更」を実行する理由は、「起動時にフォームサイズが変更するのをユーザーに分からないようにする」ためです。図6-6のActivateイベント以降で実行すると、図6-1の横長のフォームが一瞬見えてしまい、その後でカレンダーが表示されます。

Initializeイベントの後で、フォームが表示された直後に発生するのが図6-6のActivateイベントです。
  1. '========== ⇩(6) 表示時設定 ============
  2. Private Sub UserForm_Activate()
  3.  Call makeCal
  4. End Sub
図6-6

62行目「Call makeCal」では図6-11を呼出し、「フォーム上で、曜日のラベルと日付のボタンを作成」します。
Activateイベントでカレンダーボタン類を作成するのは理由があります。今回フォームカレンダーを閉じるのに「Me.Hide(図6-10の96行目)」を使用しています。つまり日付確定した後、再度日付入力のためにフォームカレンダーを開いた時に「選択したセルの日付を正しく反映」するためにActivateイベントでmakeCalプロシージャを実行する必要があるのです。
一方「UnLoad Me」で閉じる場合はmakeCalプロシージャをInitializeイベント内で実行してもOKです。但しその場合は初期状態のフォームで起動されますので、「カレンダーサイズを変更しても、次回起動時は元に戻ってしまう」ことになります。

6-2-2.カレンダー年月の移動

カレンダー上部の年月移動用スピンボタンを操作した時に呼び出されるのが図6-7です。
  1. '========== ⇩(7) スピンボタンの操作 ============
  2. Private Sub SpinButton1_Change()
  3.  If EventOff = True Then Exit Sub
  4.  dispDay = DateAdd("m", Me.SpinButton1.Value, dispDay)
  5.  Call makeCal
  6.  EventOff = True
  7.   Me.SpinButton1.Value = 0
  8.  EventOff = False
  9. End Sub
図6-7

67行目「If EventOff = True Then Exit Sub」は、この「SpinButton1_Changeイベントプロシージャ」が再帰呼び出しされた時に、同じカレンダー作成作業を重複して行わないようにするためのものです。
変数EventOffは図6-2の39行目でBoolean型で宣言されているため、初期値はFalseです。ですので通常は、このIf文が成立しないために69行目以降のコードを実行してカレンダーを作成します。
しかし72行目で「EventOff = True」としているため、73行目でスピンボタンのValue値をゼロに変えた時に発生する「SpinButton1_Changeイベントの再帰呼び出し」の時には、67行目のIf文が成立して「すぐに再帰呼び出しのプロシージャを抜け出す」ことが出来ます。

69行目「dispDay = DateAdd("m", Me.SpinButton1.Value, dispDay)」は、新たなカレンダーの年月値を変数dispDayに代入しています。スピンボタンを操作すると、初期値ゼロからMax値の「+1」またはMin値の「-1」に移動しますので、SpinButton1.Valueは「+1」または「-1」となります。その値を使いDateAdd関数で、現在表示されているカレンダーの1ヶ月先か前の月の初日を計算しています。

70行目「Call makeCal」では図6-11を呼出し、69行目で値を変更した変数dispDayを使って新たなカレンダーを作成します。

このままではスピンボタンのValue値は「+1」または「-1」になっていますので、次にスピンボタンをクリックした時にはカレンダーが移動できなくなってしまいます。ですのでValue値をゼロの中立位置に戻す必要があり、それが73行目「Me.SpinButton1.Value = 0」になります。
しかし上でも説明しましたが、Value値を変更すると再び「SpinButton1_Changeイベントプロシージャ」を呼出してしまいますので、それを避けるために72行目「EventOff = True」でフラグ変数EventOffをTrueにし、再帰呼び出し先のSpinButton1_Changeイベントプロシージャの67行目「If EventOff = True Then Exit Sub」で、すぐにプロシージャを抜け出すようにしています。
再帰呼び出し先から戻ってきたら、74行目「EventOff = False」でフラグを降ろし、次のスピンボタン操作を待ちます。

6-2-3.カレンダーサイズの変更

トグルボタンは、図6-8のように「ボタンの形状」によりValue値が異なります。凸型がFalseで凹型がTrueです。
ボタンが押され(凸→凹、凹→凸)て、そのValue値が変更した時に発生するのが図6-9のClickイベントです。なお、このClickイベントは「ボタンの形状が変わったあと」で発生します。
トグルボタンの状態
図6-8
  1. '========== ⇩(8) トグルボタンの操作 ============
  2. Private Sub ToggleButton1_Click()
  3.  Select Case Me.ToggleButton1.Value
  4.   Case False
  5.    Me.ToggleButton1.Caption = "小"
  6.    side = 20
  7.   Case True
  8.    Me.ToggleButton1.Caption = "大"
  9.    side = 24
  10.  End Select
  11.  Call makeCal
  12.  Call FormSizeSet
  13. End Sub
図6-9

81~88行目は、トグルボタンのValue値により「トグルボタンの表面文字の変更」と「カレンダーボタン類のサイズの設定」を行います。
81行目「Select Case Me.ToggleButton1.Value」で、「ボタン形状が変わった後」のトグルボタンのValue値を調べます。
82行目「Case False」でFalse(凹→凸)の時は、83行目「Me.ToggleButton1.Caption = "小"」でトグルボタン表面文字列を「小」にし、84行目「side = 20」で、カレンダーボタン類の一辺のサイズを20ポイントに設定します。
85行目「Case True」でTrue(凸→凹)の時は、86行目「Me.ToggleButton1.Caption = "大"」でトグルボタン表面文字列を「大」にし、87行目「side = 24」で、カレンダーボタン類の一辺のサイズを24ポイントに設定します。

90行目「Call makeCal」では図6-11を呼出し、84行目・87行目で設定した変数sideの値と、共通変数dispDay(表示年月)を使って「サイズの異なるカレンダー」を作成します。
91行目「Call FormSizeSet」では図6-18を呼び出し、カレンダー枠であるフォームのサイズを変更します。

なお90~91行目の実行は、どちらが先でも良いのですが、見栄えは少し違います。「FormSizeSet → makeCal」の順番だと「ボタンサイズの変更」と「フォームサイズの変更」が別々に見えますが、図6-9の順番だと「ボタンとフォームが一緒に変更」されているように見えます。
その理由は「FormSizeSet」の処理時間が「makeCal」の約1/10と短い為のようです。PCの性能にもよると思います。

6-2-4.ESCキーでのカレンダー終了

CommandButton1をクリックした時に呼び出されるのが図6-10です。ただし今回システムでは、CommandButton1はユーザーの見えない部分に隠していますのでクリックできません。
しかし、図6-4の53行目「Me.CommandButton1.Cancel = True」でキャンセルプロパティを有効にしていますので、ユーザーがESCキーを押した時には図6-10が呼び出されることになります。
  1. '========== ⇩(9) ESCキーを押した時 ============
  2. Public Sub CommandButton1_Click()
  3.  Me.Hide
  4. ' Unload Me
  5. End Sub
図6-10

96行目「Me.Hide」で、フォームカレンダーを「隠して」います。Hideを使用することで「ユーザーが選択したカレンダーサイズ」が、次回以降も反映されることになります。
なお、ユーザーがカレンダーの日付ボタンをクリックすると図7-4のmyButton_Clickが呼び出され、その中の213行目からもこの図6-10を直接呼び出しています。つまりカレンダーで日付を選択するとカレンダーが消えることになります。

一方、今回は見え消しにしてある97行目「Unload Me」を使うと、起動毎にカレンダーサイズも既定サイズに戻ります。どちらが適しているかは、使われる状況によると思います。

6-2-5.カレンダー作成

カレンダーを作成するのが図6-11です。図6-6の62行目、図6-7の70行目、図6-9の90行目から呼び出されます。
  1. '========== ⇩(10) カレンダー作成 ============
  2. Sub makeCal()
  3.  Dim Title(1 To 7) As MSForms.Label   '←週のタイトル文字列の配列
  4.  Dim i As Integer   '←日付のカウンタ変数
  5.  Dim j As Integer   '←縦方向の位置のカウンタ変数
  6.  Dim k As Integer   '←横方向の位置のカウンタ変数
  7.  Me.Label1.Caption = Format(dispDay, "yyyy年mm月")
  8.  On Error Resume Next
  9.   For i = 1 To UBound(Barray, 1) + UBound(Title, 1)
  10.    Me.Controls.Remove (Me.Controls.Count - 1)
  11.   Next i
  12.  On Error GoTo 0
  13.  ReDim Barray(1 To Day(DateAdd("m", 1, dispDay) - 1))
  14.  For k = 1 To 7
  15.   Set Title(k) = _
  16.      addLBL(cTop, cLeft + (k - 1) * side, side, side, Format(k, "aaa"), side / 1.7)
  17.  Next k
  18.  j = 1
  19.  k = Weekday(dispDay)
  20.  For i = 1 To UBound(Barray, 1)
  21.   Set Barray(i).Button = _
  22.      addBTN(cTop + side * j, cLeft + (k - 1) * side, side, side, CStr(i))
  23.   If k = 1 Then Barray(i).Button.BackColor = RGB(255, 192, 203)   '←背景:ピンク色
  24.   If k = 7 Then Barray(i).Button.BackColor = RGB(173, 216, 230)   '←背景:水色
  25.   If DateAdd("d", i - 1, dispDay) = setDay Then
  26.    Barray(i).Button.BackColor = RGB(255, 255, 0)
  27.   End If
  28.   If k = 7 Then
  29.    j = j + 1
  30.    k = 1
  31.   Else
  32.    k = k + 1
  33.   End If
  34.  Next i
  35. End Sub
図6-11

プロシージャ内で使用する変数は102~105行目で宣言していますが、その中の102行目「Dim Title(1 To 7) As MSForms.Label」は「カレンダータイトルとしての日曜~土曜までの曜日文字列」の配列宣言です。これは図6-2の35行目で宣言している「カレンダーボタンの配列」Barray()と対をなすものです。
しかしこの曜日の配列は、システム内でのクリック対象にもしていませんし要素数も変化する訳では無いので、配列化は必須ではありません。
もちろん配列を使わない場合は、111行目の「曜日の要素数」や、120~121行目の「関数の呼び出し方」は変更する必要があります。しかし今回は、曜日のタイトルを日付のボタンと同様の扱いとすることで、説明が分かり易くなると考えました。

107行目「Me.Label1.Caption = Format(dispDay, "yyyy年mm月")」は、カレンダー年月表示用のラベルに、年月を書き込んでいます。

111~113行目のFor~Nextでは「古いカレンダーのボタン(+曜日のラベル)」を削除しています。
これは、カレンダー年月を移動させた際、配列Barrayについては117行目のReDimで初期化をしているのですが、ボタンの実体はフォーム上に残ったままです。ですのでボタン類を削除しないと、図6-12のように「ボタンの上にボタンを積み重ねる」形になり、古い月の日付ボタンが見えてしまいます。
ボタンを削除しないと積み重なる
図6-12

そこで111行目「For i = 1 To UBound(Barray, 1) + UBound(Title, 1)」で、カウンタ変数iを「ボタンの数」+「週のラベルの数」だけ回しながら、112行目「Me.Controls.Remove (Me.Controls.Count - 1)」でフォーム上に追加した最後のコントロールから削除していきます。For~Nextで前回作った数だけ繰り返すことで、作った全てのコントロールを削除することが出来ます。なおRemoveへのインデックスはゼロ始まりのため「-1」調整しています。

なお、Removeメソッドは「実行時に追加されたコントロールを削除」するものですので、「デザイン時に追加したコントロールを削除しようとするとエラーが発生」します。「実行時に追加」されたものだけをRemoveするために削除する個数を「UBound(Barray, 1) + UBound(Title, 1)」としているのですが、フォームを最初に起動した時には「まだボタンが無い」状態で、配列Barrayの要素数も決まっていません。そのため111行目のFor文でエラーが発生してしまうため、109行目「On Error Resume Next」でエラーをスルーさせています。
なお、週のラベルは月の移動に無関係なので「毎回削除する必要は無い」のですが、カレンダーサイズ変更の時には必要となりますので、少し無駄ですが今回は毎回削除する仕様としました。

117行目「ReDim Barray(1 To Day(DateAdd("m", 1, dispDay) - 1))」では、ボタンを格納する配列Barrayの要素数を設定しています。ボタンの数は、その表示月(dispDay)の月数ですので、DateAdd関数で次月のゼロ日目(=今月の月末日)の日にちを計算しています。

119~122行目では「日曜~土曜までの曜日のラベル」を作成しています。
119行目「For k = 1 To 7」で、カウンタ変数kを1週間=7日分だけ回します。
120~121行目「Set Title(k) = addLBL(cTop, cLeft + (k - 1) * side, side, side, Format(k, "aaa"), side / 1.7)」では、図6-17のaddLBL関数プロシージャを呼出し、曜日のラベルを作成しています。引数として、図6-13のように6個を渡しています。
No.引数名ラベルのプロパティ
1TSingleTop
2LSingleLeft
3HSingleHeight
4WSingleWidth
5LnameStringCaption
6SzSingleFont.Size
図6-13

1番目のTopプロパティには図6-4の44行目で計算済みの「cTop」を、2番目のLeftプロパティには43行目で設定したcLeft値を基点にし、ボタンの一辺の長さ(変数side値)分だけ移動(k - 1)しながら並べていきます。またHeight、Widthプロパティは、ボタンの一辺の長さ(side値)にします。

ラベルの文字は「日~土」の各1文字とするのですが、通常は「日~土の文字列の配列」を作って代入していくのが一般的なようです。今回は別な方法として「Format(k, "aaa"):k は 1~7 」という式を使用しています。
VBAでの日付計算は、1899/12/31を1としています(ワークシート側は、1900/1/1がシリアル値 1 )。運の良いことに、その1を表す「1899/12/31」は「日曜日」なのです。曜日の文字列はFormat(日付, "aaa")で得られますが、日付のところにWeekDay関数と同じく1を入れれば「日」、2を入れれば「月」という文字列が得られる事になります。

6番目の「Font.Size」には、「side / 1.7」という値を代入しています。これは、カレンダーを実際に作ってみて違和感の無いサイズにTry & Errorをして決めました。

124行目以降では、日付のボタンを作成しています。
124行目「j = 1」は、日付ボタンの行位置の初期値を1と置きます。
125行目「k = Weekday(dispDay)」は、カレンダー初日の曜日値を取得し、変数kに代入します。
127行目「For i = 1 To UBound(Barray, 1)」は、カウンタ変数iを1から表示月の月末日まで回します。つまり変数iは「日付」を表す事になります。

129~130行目「Set Barray(i).Button = addBTN(cTop + side * j, cLeft + (k - 1) * side, side, side, CStr(i))」では、図6-16のaddBTN関数プロシージャを呼出し、日付ボタンを作成しています。引数として、図6-14のように5個を渡しています。
No.引数名ボタンのプロパティ
1TSingleTop
2LSingleLeft
3HSingleHeight
4WSingleWidth
5BnameStringCaption
図6-14

1番目のTopプロパティには図6-4の44行目で計算済みの「cTop」に対し、ボタンの高さ「side」に行数「j」を掛けた値だけ下に表示させます。またLeft方向は、曜日のラベルと同様に43行目で計算したcLeft値に、ボタンの一辺の長さ(変数side値)分だけ移動しながら並べていきます。またHeight、Widthプロパティは、ボタンの一辺の長さを指定します。

5番目のボタンのCaptionには「日付」を表示しますので、日付を意味する「変数i」を文字列型に変換(CStr関数)してから指定します。
これらの引数を与えて作られるボタンをクラスのButtonプロパティに設定します。こうする事で、登録したボタンをクリックしたイベントを得られることが出来ます。誤解を恐れずに絵にしてみると図6-15のような繋がりになり、最終的にボタンのClickイベントを発生させ、どのボタンがクリックされたかを取得することが出来ます。

クラスとのつながり
図6-15

132行目「If k = 1 Then Barray(i).Button.BackColor = RGB(255, 192, 203)」では、k(曜日)が 1(一番左の列=日曜日)の場合に、ボタンの背景色をピンク色にしています。
133行目「If k = 7 Then Barray(i).Button.BackColor = RGB(173, 216, 230)」では、k(曜日)が 7(一番右の列=土曜日)の場合に、ボタンの背景色を水色にしています。
ここで使っている「Button」は、図7-2の「Public Property Get Button() As Control」の方のプロパティになります。

135~137行目では、setDay(今日の日付、または日付入力セルに記入されていた日付)をカレンダーのボタンの中から探し、存在したらその日のボタンの背景に色を付けます。
135行目「If DateAdd("d", i - 1, dispDay) = setDay Then」で、ボタン上の日付文字列(i)と表示カレンダー初日(dispDay)から「カレンダー上の日付」に変換(イコールの左辺)し、setDay(イコールの右辺)と比較をしています。もしIf文が成立(=処理している日がsetDay)した時は136行目「Barray(i).Button.BackColor = RGB(255, 255, 0)」で、ボタン背景色を黄色にしています。

139~144行目では、ボタンの位置を移動させるために、位置の変数「j=行方向」「k=列方向」を変更しています。
139行目「If k = 7 Then」は、現在の列位置が「一番右=土曜日」だった時に140~141行目を実行し、それ以外の時(日~金)には143行目を実行させます。
140行目「j = j + 1」では、行位置を一つ下に移動させます。141行目「k = 1」では列位置を一番左(日曜日の列)にします。ちょうどタイプライターのCrLf(キャリッジリターン/ラインフィード)に相当する部分です。右端に達したので「左端に戻し、1行下に移動」させています。

143行目「k = k + 1」は、まだ右端に達していないため、一つ右側(列を+1)に移動させます。

6-2-6.ボタン作成モジュール

図6-11の130行目から呼び出される「ボタンを作成するモジュール」が図6-16です。5つの引数を受取りますが、その内容は図6-14の通り、ボタンの位置とサイズを決める値+ボタン表面の文字列です。
  1. '========== ⇩(11) ボタン作成モジュール ============
  2. Private Function addBTN(T As Single, _
  3.             L As Single, _
  4.             H As Single, _
  5.             W As Single, _
  6.             Bname As String) As MSForms.CommandButton
  7.  Set addBTN = Me.Controls.Add("Forms.CommandButton.1")
  8.  With addBTN
  9.   .Top = T
  10.   .Left = L
  11.   .Height = H
  12.   .Width = W
  13.   .Caption = Bname
  14.  End With
  15. End Function
図6-16

157行目「Set addBTN = Me.Controls.Add("Forms.CommandButton.1")」は、このフォーム上(Me)に「CommandButton」を追加します。追加したボタンオブジェクトは、関数の戻り値であるaddBTNにセットします。

159~165行目では、追加したボタン(159行目の「With addBTN」)のプロパティを変更していきます。
160行目「.Top = T」では上下方向の位置を、161行目「.Left = L」では左右方向の位置を決めます。
162行目「.Height = H」ではボタンの高さを、163行目「.Width = W」ではボタンの幅を調整します。なお、無指定だと既定値(環境により違う可能性有り)の「高さ24 × 幅72(単位ポイント)」になるようです。
164行目「.Caption = Bname」は、ボタン表面文字列(今回は日付)を設定します。

寄り道
今までの項も含め、別のプロシージャに引数で値を渡す場合は「データ型を合わせて渡す」ようにしています。データ型が合わないとエラーが出て進めないからですが、例えば130行目から呼び出すaddBTN関数(図6-16)の第5引数はString型指定なので、呼び出す側では「CStr(i)」と、Integer型をString型に変換してから渡しています。
この時の受け取る側の指定は「Bname As String」です。byRef か byVal かを指定しない時の既定値は byRef ですので、正確に書くと「byRef Bname As String」です。

これを「byVal Bname As String」として受け取ることにすると、呼び出す側は「型を気にせず」にInteger型のままの「i」を渡せるようになります。呼び出す側でカッコを付けるとbyVal扱いになりますので「(i)」としても同じです。
byValには「値渡し」という機能と共に、自動型変換のような機能もあるようです。

もちろん「byValで渡すと呼び出し元の変数値は変化しない」というのがメインの機能で、この「型変換」という特徴はマイクロソフトのどのページを見ても説明がありませんので「暗黙」の機能のようです。
一見便利そうな機能です。しかしデータ型を気にしている理由の1つは、プログラムのミスを発見できる事だと思っているので、それに目隠しをしてしまうような機能は「何が起こるか分からない怖さ」があります。もしbyValを使用する場合は充分気を付けた方が良いと思います。

6-2-7.ラベル作成モジュール

図6-11の121行目から呼び出される「ラベルを作成するモジュール」が図6-17です。6つの引数を受取りますが、その内容は図6-13の通り、ラベルの位置とサイズを決める値+ラベルの文字列+フォントサイズです。
  1. '========== ⇩(12) ラベル作成モジュール ============
  2. Private Function addLBL(T As Single, _
  3.             L As Single, _
  4.             H As Single, _
  5.             W As Single, _
  6.             Lname As String, _
  7.             Sz As Single) As MSForms.Label
  8.  Set addLBL = Me.Controls.Add("Forms.Label.1")
  9.  With addLBL
  10.   .Top = T
  11.   .Left = L
  12.   .Height = H
  13.   .Width = W
  14.   .Caption = Lname
  15.   .Font.Size = Sz
  16.   .TextAlign = fmTextAlignCenter
  17.  End With
  18. End Function
図6-17

177行目「Set addLBL = Me.Controls.Add("Forms.Label.1")」は、このフォーム上(Me)に「Label」を追加します。追加したオブジェクトは関数の戻り値であるaddLBLにセットします。

179~187行目では、追加したラベル(179行目「With addLBL」)のプロパティを変更していきます。
180行目「.Top = T」では上下方向の位置を、181行目「.Left = L」では左右方向の位置を決めます。
182行目「.Height = H」ではラベルの高さを、183行目「.Width = W」ではラベルの幅を調整します。なお無指定だと既定値(環境により違う可能性有り)の「高さ18 × 幅72(単位ポイント)」になるようです。
184行目「.Caption = Lname」では、ラベルの文字列(今回は曜日)を設定します。
185行目「.Font.Size = Sz」はラベルのフォントサイズを設定し、186行目「.TextAlign = fmTextAlignCenter」では文字位置を中央揃えにしています。最後のTextAlignだけは引数に影響されずに直接指定しています。

6-2-8.フォームサイズ変更モジュール

図6-4の57行目、図6-9の91行目から呼び出される「フォームのサイズを変更」するのが図6-18です。
  1. '========== ⇩(13) フォームサイズ変更 ============
  2. Private Sub FormSizeSet()
  3.  Me.Height = cTop + 7 * side + cLeft + (Me.Height - Me.InsideHeight)
  4.  Me.Width = cLeft + 7 * side + cLeft + (Me.Width - Me.InsideWidth)
  5. End Sub
図6-18

フォームの各部寸法は、図6-19のようになっています。
フォームのサイズ
図6-19

縦方向(図6-19の左側)については、カレンダーの曜日が始まる高さcTopを図6-4の44行目で決めています。カレンダーの曜日文字・日付ボタンの高さは変数sideで、合わせて7行分あります。またカレンダーの四隅を同じcLeft値(図6-4の43行目で指定)とすることで、カレンダーがバランス良く見えます。
なお、フォーム上のコントロールの位置は「InsideHeight、InsideWidthベース」となりますが、設定できるのは「Height、Width」という「フォームの外側の寸法」になります。左右方向は線2本分の幅しか変わりませんが、上下方向はフォームのタイトル部があり、約30ポイントもの差がありますので無視する事はできません。

そこで193行目「Me.Height = cTop + 7 * side + cLeft + (Me.Height - Me.InsideHeight)」では、差である「Me.Height - Me.InsideHeight」を加えた値を、フォーム全体の高さとして設定します。
また左右方向(図6-19の右側)も、194行目「Me.Width = cLeft + 7 * side + cLeft + (Me.Width - Me.InsideWidth)」と、差「Me.Width - Me.InsideWidth」を加えた値を幅に設定しています。

7.クラスモジュール(Class1)

7-1.ボタンオブジェクトの設定

クラスモジュール宣言部では図7-1のように、ボタンのイベントを取得するためにWithEventsの宣言をしています。
198行目「Private WithEvents myButton As MSForms.CommandButton」では、オブジェクト変数myButtonを「MSForms.CommandButton」の型として宣言しています。そして図6-2の35行目「Dim Barray() As New Class1」でオブジェクトを生成し、図6-11の129~130行目「Set Barray(i).Button = addBTN(・・・)」で日付ボタンを登録することで、図7-4の「myButton_Clickイベント」が使えるようになります。
  1. '========== ⇩(14) WithEventsの宣言 ============
  2. Private WithEvents myButton As MSForms.CommandButton
図7-1

プロパティの値を取得するプロシージャが図7-2です。
本システムで言えば、例えば図6-11の136行目で「Barray(i).Button.BackColor = RGB(255, 255, 0)」と「本日のボタン背景を黄色」に設定していますが、クラス側から見れば黄色の背景色プロパティを取得していることになります。
  1. '========== ⇩(15) プロパティ値の取得 ===========
  2. Public Property Get Button() As Control
  3.  Set Button = myButton
  4. End Property
図7-2

プロパティのオブジェクトを設定するプロシージャが図7-3です。引数として「オブジェクトへの参照を表す変数addBTN」を指定します。
本システムで言えば、図6-11の129~130行目で「Set Barray(i).Button = addBTN(・・・)」と、作成したボタンオブジェクトを登録(=設定)します。
  1. '========== ⇩(16) プロパティのオブジェクトを設定 ============
  2. Public Property Set Button(ByVal addBTN As Control)
  3.  Set myButton = addBTN
  4. End Property
図7-3

7-2.クラスに登録されたボタンのClickイベント

登録したボタン(=カレンダーの日付ボタン)をクリックした時に呼び出されるのが図7-4のイベントプロシージャです。カレンダーの日付をクリックしたのですから、フォームカレンダーの役割は終了し、クリックされた日付を戻すことになります。
  1. '========== ⇩(17) ボタンのClickイベント ============
  2. Private Sub myButton_Click()
  3.  OrgValue = dispDay + CInt(myButton.Caption) - 1
  4.  Call UserForm1.CommandButton1_Click
  5. End Sub
図7-4

このmyButton_Clickでは、クリックされたボタンの表面文字列を「myButton.Caption」として取得することが出来ます。他にはクリックしたボタンの位置なども取得できますが、今回は「ボタン上の日付は重複しない」ので表面文字列を使用します。
212行目「OrgValue = dispDay + CInt(myButton.Caption) - 1」では、そのボタン表面の日付文字列をCInt関数で数値に変換し、表示カレンダーの初日(dispDay:Date型)に加えることで「クリックされたボタンの年月日」を計算し、共通変数OrgValueに代入します。

213行目「Call UserForm1.CommandButton1_Click」では、図6-10を呼び出し、フォームカレンダーを終了させます。すると制御は図5-2の31行目「CalSelect = OrgValue」に移り、212行目で代入した日付がワークシートのSelectionChangeイベントに戻ります。
最後に図4-2の6行目「Target(1).Value = Workbooks("it-078.xlam").Application.Run("CalSelect", Target(1))」で戻り値(日付)を選択したセルに書き込みます。

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

このマクロ付ファイル(サンプルファイル)をExcelのアドインに登録することで、今回の「フォームカレンダー」を他のブックから呼び出して使うことが出来ます。アドイン方法については「年賀状リスト等の宛名検索と追記 アドイン登録」を参照下さい。
なお、ボタンを押して何かを実行するシステムではないので、「アドイン保存したファイル名を有効にする」まででOKです。

9.最後に

日程を管理するアプリの検討を始めると、すぐに「日付をどうやって入力するか」の問題に突き当たります。アプリに適したカレンダーをその都度作っていたら大変ですので、その解決策の1つが今回の「マクロ側から日付ボタンを並べる」カレンダーになるのでは、と思います。
今回はボタンのサイズを変えるだけの内容でしたが、工夫すれば複数ヶ月のカレンダーも可能でしょうし、またカレンダー以外にも簡単な棒グラフや表くらいなら作れそうに思えます。


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