2021/10/10

ラベルカレンダーをクリックし日付入力




1.背景

今までこのサイトでは、ユーザーの日付入力を補助するカレンダーを2つ紹介してきました。
 「セルへの日付入力をカレンダー日付クリックで選定」(図1-1の左側)
 「図形カレンダーをクリックし日付入力」(図1-1の右側)
以前のカレンダー
図1-1(以前紹介したクリック式カレンダー)

しかし「クラスを使うのはハードルが高い」とか「画像は処理速度が遅い」などデメリットもありますので、もう少し簡単に選択式カレンダーが作れないか考えた結果、ラベルに数字を並べてカレンダーにする方法を思いつきました。(既に思いついていた人がいたら、ごめんなさい)
今回は、フォーム上のラベルにカレンダーを作り、ラベル上をクリックした位置からユーザーが指定する日付を判断し、作業用シートのセルへ日付を出力するものを紹介します。

2.システム概要

今回システムは、作業用シート上の日付入力セルに「カレンダーを呼び出すマクロ」を組み込んでおき、ユーザーが操作した時にカレンダーが起動し、指定する日をクリックすると日付入力セルに日付が出力されるものです。
作業用シートに組み込むマクロについては「作業用シートのマクロ」の項で説明します。
なお、カレンダー部分(UserForm1)を作業用のブックに事前に組み込んでおく必要があります。

呼び出す方法としては、図2-1のように「セルをダブルクリック①」や「ボタンをクリック②」等が考えられます。例えばセルをダブルクリック①すると、カレンダーが表示③されます。
カレンダー起動と年月移動
図2-1

カレンダー上部の「スクロールバー」や「今月ボタン」を操作④することで、ユーザーが指定したい年月にカレンダーを移動することができます。スクロールバーの「スクロール矢印(図2-2参照)」部をクリックすると1か月ずつ移動し、レール部をクリックすると1年移動します。また「今月ボタン」をクリックすることで、今月にジャンプします。

スクロールバー各部の呼び名
図2-2

なお、カレンダー起動直後に表示されるカレンダーは、日付入力セルが空白・日付以外の文字列の場合には今月を表示します。入力セルに日付が既に入っていた場合は、その日付を含む年月がまず表示されます。

希望の年月に移動ができたら、図2-3左のようにカレンダー上の日付をクリック⑤すると、指定した日付が一瞬(今回設定では0.3秒)赤文字⑥になった後、カレンダーは消えます。消えた後、ユーザーが指定した日付けが日付入力セルに書き込まれます。
なお、日付をクリックせずにカレンダー右上の×印をクリックすると、日付入力セルは元の値のままで変更無しとなります。
指定日をクリックしセルへ出力
図2-3

今回はラベルに数字を並べてカレンダーを作っているため、図2-4のようにフォントサイズによってカレンダーの大きさが違ってきます。サンプルファイルではフォントサイズ=18ポイントに設定しています。
フォントサイズでカレンダーサイズ変更
図2-4

3.プログラムの流れ

作業用シートに埋め込まれたマクロから、フォームモジュールのUFstartが呼び出され、カレンダー計算でLabelにカレンダーを作成した後にフォームを起動します。カレンダー計算の目に見える出力はフォーム上のLabelカレンダーですが、同時に配列の形にしたカレンダー(Calarray)を内部的に作ります。
フォーム上のスクロールバー・今月ボタンを操作すると、カレンダー計算をやり直し、新しいカレンダーを表示します。カレンダーを更新するたびに配列カレンダーも更新します。
プログラムの流れ
図3-1

ユーザーがカレンダーの日付をクリックするとLabelのMouseDownイベントが発生しますので、そのクリックした位置を計算し、配列カレンダーCalarrayと突き合わせをして「クリックした日付」を取得します。そして、現在表示しているカレンダーの年月と合わせて日付の形にし、作業用シートの日付入力セルに出力します。

4.作業用シートのマクロ(サンプルファイルではSheet1)

日付を入力する作業用シートを図4-1とします。
 ①C4セルをダブルクリックするとカレンダーを表示し、同じC4セルに指定した日付を入力
 ②ボタンをクリックするとカレンダーを表示し、E4セルに指定した日付を入力
するためのマクロを考えます。
作業用シート
図4-1

まず①のマクロは図4-2のように、ワークシートのBeforeDoubleClickイベントを使用します。
  1. '========== ⇩(1) ダブルクリックでカレンダーを表示 ============
  2. Private Sub Worksheet_BeforeDoubleClick(ByVal Target As Range, Cancel As Boolean)
  3.  If Target.Address = Range("C4").Address Then
  4.   Cancel = True
  5.   Target.Value = UserForm1.UFstart(Target.Value)
  6.  End If
  7. End Sub
図4-2

3行目「If Target.Address = Range("C4").Address Then」で、ダブルクリックしたセルのアドレスを調べ、「C4セル」の場合に4~5行目を実行します。
なお「If Target.Address = "C4" Then」とすると、C4セルで引っ掛かってくれません。せめて「If Target.Address = "$C$4" Then」であれば引っ掛かります(Cが小文字なら、やはりNGです)が、このような部分に気を遣うのはもったいないので、3行目のように「Address値同士を比較」した方が確実です。

4行目「Cancel = True」ではCancelを有効にし、ダブルクリックによる「セルへの編集」を中止します。このコードが無くてもカレンダーは起動しますが、カレンダーが閉じた後に「セル編集モード」になってしまいます。

5行目「Target.Value = UserForm1.UFstart(Target.Value)」で、UserForm1上の「UFstart」関数プロシージャ(図5-9)を呼び出します。引数には、カレンダー側に渡す「セルの値」を指定します。引数値が日付の場合、カレンダーの年月は引数日付の年月を表示した状態で開きます。
また今回は「ダブルクリックするセル = 日付を入力するセル」であるため、BeforeDoubleClickイベントの引数のTargetを使っていますが、例えば「C4セルをダブルクリックして、C5セルに日付を入れる」ようなことも可能です。
「UFstart」関数プロシージャは、カレンダーで選択した日付を戻してくれますので、その値を左辺の「Target.Value」に設定します。なお、カレンダーの日付を選択せずに「カレンダーを閉じた時」には、指定した引数(右辺のTarget.Value)をそのまま戻して来ますので、日付セルの「値はそのまま」となります。

次に②のボタン(フォームコントロール)による操作では、ボタンに登録したマクロ(図4-3)を実行します。
  1. '========== ⇩(2) ボタン操作でカレンダーを表示 ============
  2. Public Sub HizukeClick()
  3.  Range("E4").Value = UserForm1.UFstart(Range("E4").Value)
  4. End Sub
図4-3

単純に、11行目「Range("E4").Value = UserForm1.UFstart(Range("E4").Value)」で、図5-9のUserForm1上の「UFstart」関数プロシージャを呼び出し、戻し値であるカレンダー上で選択した日付を「日付入力セル(E4セル)」に書き込みます。

5.ユーザーフォーム(UserForm1)

5-1.レイアウト

カレンダーを操作するためのフォームは、図5-1のようなレイアウトにしました。
フォームレイアウト
図5-1

カレンダー本体用にLabel1を下半分に配置し、その上に曜日表示用のLabel2を配置します。
また年月移動用にScrollBar1、今月にジャンプする為のCommandButton1を上側に配置し、その中間に、表示しているカレンダーの年月表示用のLabel3を置いています。

今回は、Label1に数字を並べてカレンダーの形にしており、そのLabel1コントロールの大きさは(設定した文字を全て表示させるには)文字の数が同じでも「Fontの種類(今回は『MSゴシック』に固定)」や「Fontのサイズ」で変わります。そのためLavel1とLabel2のサイズは設定するFontサイズによりマクロ側から調整を行っています。ですので、結構適当な位置・大きさで配置してありますが心配は不要です(フォントサイズを決めたら、微調整は必要と思います)。

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

5-2-1.起動時初期設定

フォーム内で共通して使用する変数・定数を図5-2のように宣言部で宣言しています。
  1. '========== ⇩(3)) 変数宣言 ============
  2. Dim orgDate As Variant      '既入力値・新出力値
  3. Dim UFdate As Date        '表示カレンダーの初日
  4. Dim IsMoveSB As Boolean     'スクロールバー設定中のフラグ
  5. Dim Calarray(1 To 6, 1 To 7) As Integer    '表示カレンダー値の配列
  6. Const TabWidth As Single = 36    'Tabの幅(単位:ポイント)
  7. Const Fsize As Single = 18    'LabelのFontサイズ(10.5~24までは確認済み)
図5-2

15行目「Dim orgDate As Variant」は、作業用シートの日付入力セルに記入されていた値を一時保存するためのものです。カレンダーが終了する時にこの変数orgDate値を戻しますが、ユーザーが日付を指定した時には、その日付を変数orgDateに代入することで戻り値をカレンダー指定値に変更しています。
16行目「Dim UFdate As Date」は、表示されているカレンダーの初日です。
17行目「Dim IsMoveSB As Boolean」は、スクロールバーをマクロ側から設定する時に立てるフラグで、このフラグが立っている間は、スクロールバーのChangeイベント内の処理をスルーさせています。
18行目「Dim Calarray(1 To 6, 1 To 7) As Integer」は、カレンダーの配置(縦6行×横7列)の中の、どこに何日が設定されているかを配列内に記憶させておくものです。ユーザーがどの日付をクリックしたかの計算を楽にするために使用しています。

20行目「Const TabWidth As Single = 36」は、フォーム上のLabel内で「Tabだけ入力した時に移動する距離(ポイント単位)」です。この値についての詳細は「よりみち」を参照下さい。
21行目の「Const Fsize As Single = 18」は、カレンダーのフォントサイズを指定しています。この値を変更することで図2-4のようにカレンダーサイズが変わります。

寄り道
今回Labelに数字を並べてカレンダーを作るに当たり、2つの事を考えました。
 ・フォントをどうするか
 ・数字と数字の間を何で空けるか

1つ目のフォントは直感的に「MS P系」では無く、「MSゴシック」または「MS明朝」と思いました。カレンダー上に数字をきれいに並べるためには、文字の横幅が同じである方が良いと考えたからです。フォントの種類はものすごく多く、他にも横幅が統一できるフォントがあるのかもしれませんが、今回は「MSゴシック」に決め打ちしました。

そこでMSゴシックのフォントサイズと横幅について調べてみると、単純に「縦寸法÷2=横寸法」となる訳ではなく「ピクセルで換算し小数点以下は切り上げる」ようです。表示できるのは基本的にはピクセル単位なので当然ですが、一応調べてみた結果が図5-3の「横サイズ(実測)」です。
但しこの表に載せてある値は「ディスプレイ拡大率100%」のPCでの値で、拡大率を上げると横サイズも微妙にズレるようです。これは、見かけ上「1ピクセル以下の表示が出来る」ために発生する現象だと思われます。
文字サイズとTABサイズ
図5-3

各フォントサイズ毎の横サイズが把握できたところで、次に「数字と数字の間(=日付と日付の間)」を何で空けるかを考えました。単純に考えれば「スペース」が良いのですが、試してみると図5-3の横サイズよりも小さく「通常の半角文字幅の約60%幅」で、しかもフォントサイズにより微妙に変わってしまいます。全角スペースも「通常の半角文字幅の約130%幅」で、サイズにより変化します。
その他の文字では「_(アンダースコア)」は半角スペースと同じような感じですし、「.(ピリオド)」は「通常の半角文字幅の約42%幅」と、どれもラベルのカレンダーの隙間に使うのは難しそうです。
スペースっぽくて普通の文字と同じ幅になるのは「・(ナカグロの半角)」「-(マイナス)」くらいです。しかし「・」や「-」では、カレンダーがゴチャゴチャに見えるため「隙間用」には向かないようです。

そこで思いついたのが「TAB」です。TABは一定の幅を持つ文字とは異なりますが、ワープロのように一定間隔で文字を置いていくことが可能です。「通常は1タブで4間隔」だから、横サイズで4か8(全角4つかもしれないと考えたので)だろう と予測し調べはじめました。その調べた結果が、図5-3の「TABに入る個数」です。

予測を完全に裏切られ「どうなっているんだ?」と表にまとめてみると、「文字の横サイズ×個数」つまり「TABで移動する最大値」はほぼ一定の値になることに気が付きました。恐らく「偶然に一定」になった訳では無く、フォントサイズには関係なく「36ポイント(=48ピクセル)」なのではないか、と推定しています。

今回システムでは、図5-2の20行目で「TABの最大幅=36ポイント」として設定していますが、バージョンやO/Sで異なるのかもしれません。もし異なっていたら(表示するカレンダーの横が切れたり余ったりするので気が付くはずです)、20行目の定数値を調整して下さい。

フォーム起動時に最初に呼び出されるInitializeイベントプロシージャが、図5-4です。
  1. '========== ⇩(4) フォーム初期設定 ============
  2. Private Sub UserForm_Initialize()
  3.  Me.Label1.Font.Size = Fsize
  4.  Me.Label1.Left = Fsize / 2
  5.  Me.Label1.Font = "MS ゴシック"
  6.  Me.Label1.Width = TabWidth * 6 + 2 * Fsize / 2 + (2)
  7.  Me.Label1.Height = 6 * Fsize
  8.  Me.Label2.Font.Size = Fsize
  9.  Me.Label2.Left = Fsize / 2
  10.  Me.Label2.Width = TabWidth * 6 + 2 * Fsize / 2 + (2)
  11.  Me.Label2.Height = Fsize
  12.  Me.Label2.Caption = "日" & vbTab & "月" & vbTab & "火" & vbTab _
  13.              & "水" & vbTab & "木" & vbTab & "金" & vbTab & "土"
  14.  Me.Label3.Font.Size = 16
  15.  Me.Label3.Font.Bold = True
  16.  Me.CommandButton1.Caption = "今月"
  17.  Me.Width = Me.Label1.Width + 2 * Fsize / 2 + (Me.Width - Me.InsideWidth)
  18.  Me.Height = Me.Label1.Top + Me.Label1.Height + (Me.Height - Me.InsideHeight) + Fsize / 2
  19.  IsMoveSB = True
  20.   Me.ScrollBar1.Max = 12
  21.   Me.ScrollBar1.Min = -12
  22.   Me.ScrollBar1.Value = 0
  23.   Me.ScrollBar1.LargeChange = 12
  24.   Me.ScrollBar1.SmallChange = 1
  25.  IsMoveSB = False
  26. End Sub
図5-4

まず、レイアウトの考え方を説明します。
今回カレンダーは「TABを使って日付表示位置を定めている」ため、一番左列の日曜日の数字は、どうしても「Labelの左端一杯に表示」されてしまいます。Labelをフォーム一杯にピッタリ寄せて表示しても機能的にはOKなのですが、日付が枠ギリギリに表示されてしまいバランスが良くありません。
ですので図5-5のように、フォームの端に対して少しスペースを空けた位置にLabelを表示しています。(図5-5では、フォームとの関係が分かり易いように、LabelのBorderStyleをONにしています。)
この様にフォームとLabelの間にスペースを空ける事で、あたかもカレンダーが整然と並んでいる様に見せることが出来ます。
カレンダー台座であるフォームとLabelの位置関係
図5-5

26~30行目は、カレンダー本体であるLabel1の設定です。
26行目「Me.Label1.Font.Size = Fsize」は、Label1のフォントサイズを定数Fsize(図5-2の21行目で設定。サンプルファイルでは18ポイント)に設定しています。
27行目「Me.Label1.Left = Fsize / 2」は、図5-6のようにフォームに対し、カレンダー本体のLabel1を「Fsize / 2(=数字の横サイズ)」分だけズラした位置に配置しています。これは上述した「カレンダーが整然」と並んでいるように見せる為です。
カレンダーの各寸法
図5-6

28行目「Me.Label1.Font = "MS ゴシック"」は、Label内の文字の横サイズが同じ「等幅フォント」にしています。カレンダーが整然と並んでいるように見せる為と、位置計算を簡単にするためです。
29行目「Me.Label1.Width = TabWidth * 6 + 2 * Fsize / 2 + (2)」は、Label1の横サイズを図5-7のように定めています。
カレンダーは週7日ですが、TABで表示位置を動かしているのは左端から6箇所分「TabWidth * 6」です。一番右列(土曜日列)は、半角文字2個分ですので「2 * Fsize / 2」を加えます。
最後の「+ (2)」は「文字とLabel枠の間のスペース」です。1行当たりの文字数によっても異なるようなのですが、今回の場合は29行目コードのように「2(ポイント)」あれば大丈夫そうです。尚これが無いと「一番右端の文字が次の行」に送られてしまいます。
30行目「Me.Label1.Height = 6 * Fsize」は、縦方向の高さを6行分の寸法に設定しています。これはカレンダーは6行あれば、どんな年月でも日付を重ねなくても表示できるためです。
但し、フォントサイズが大きくなる(18超)と、一番下の行の数字の下側が少しだけ欠ける現象が出ます。フォントサイズを大きくする場合は、縦方向も余裕代を取る必要がありそうです。
カレンダー内部の各寸法
図5-7

32~37行目は、カレンダーの曜日タイトルであるLabel2の設定です。
32行目「Me.Label2.Font.Size = Fsize」は、Label1と同じフォントサイズを設定しています。
33行目「Me.Label2.Left = Fsize / 2」は、カレンダー本体と列位置を合わせるために、Label1のLeftプロパティと同じ値を設定しています。
34行目「Me.Label2.Width = TabWidth * 6 + 2 * Fsize / 2 + (2)」は、横幅をLabel1に合わせています。
35行目「Me.Label2.Height = Fsize」は、曜日タイトルですので1行分の高さを指定します。
36~37行目「Me.Label2.Caption = "日" & vbTab & "月" & vbTab & "火" & vbTab & "水" & vbTab & "木" & vbTab & "金" & vbTab & "土"」では、曜日タイトルに、「日・月~土」までの文字列をTABを間に入れて設定しています。

なお、カレンダー本体のLabel1と位置・サイズを合わせていますので、32~34行目は例えば「Me.Label2.Font.Size = Me.Label1.Font.Size」のような設定方法でもOKと思います。

39~40行目は、カレンダーの年月を表示するLabel3の設定です。Label3は、カレンダー側のフォントサイズ等とは連動させる必要は無く、どのサイズでも合うような形に設定しています。
39行目「Me.Label3.Font.Size = 16」では、フォントサイズを16ポイントにしています。
40行目「Me.Label3.Font.Bold = True」では、フォントを太文字にしています。

42行目「Me.CommandButton1.Caption = "今月"」は、「今月ボタン」の表面文字列を設定しています。

44~45行目は、Label1のカレンダーが整然と表示されるように、表示されるフォームのサイズを決めています(図5-6)。
44行目「Me.Width = Me.Label1.Width + 2 * Fsize / 2 + (Me.Width - Me.InsideWidth)」では、カレンダー本体の幅(Me.Label1.Width)の両端に文字横幅1個分ずつのスペース(2 * Fsize / 2)を取ります。
なお、Label1等はフォームの内側部分(InsideWidth)に描画されているため、フォームの枠寸法のような「+ (Me.Width - Me.InsideWidth)」が無いと、Label1などがフォームの外側にはみ出して描画される事になってしまいます。
45行目「Me.Height = Me.Label1.Top + Me.Label1.Height + (Me.Height - Me.InsideHeight) + Fsize / 2」は、Label1までの高さ+Label1の高さ、それにフォームの枠寸法である「(Me.Height - Me.InsideHeight)」に加えて、整然とカレンダー日付が並んで見えるようにLabel1の下側に文字幅分を足して、フォームの高さとしています。

48~52行目では、カレンダー年月を移動するスクロールバーの設定をしています。設定の途中でスクロールバーのValue値が変わるとChangeイベントが発生してしまいますので、47行目「IsMoveSB = True」で「マクロ側からスクロールバーの設定を操作しているフラグ」を立て、Changeイベントの処理をスルーさせるようにしています。

今回のスクロールバーは図5-8のように、スクロール矢印をクリックすれば1ヶ月移動し、レール部をクリックすれば1年移動します。そして移動後すぐにスクロールボックスは中立の位置に戻るようにしています。
年月移動のスクロールバーの遷移
図5-8

すぐに中立位置に戻りますので、例えばレール部を3回クリックすれば3年移動することになります。ですのでスクロールバーの最大値・最小値は1年分で良いことになります。
48行目「Me.ScrollBar1.Max = 12」では最大値を12(ヶ月)に設定し、49行目「Me.ScrollBar1.Min = -12」では最小値を-12(ヶ月)にします。また、スクロールボックスを中立位置にするため、50行目「Me.ScrollBar1.Value = 0」とします。
また、レール部をクリックした時に12ヶ月移動するように51行目「Me.ScrollBar1.LargeChange = 12」で最大移動量を12に設定し、スクロール矢印で1ヶ月移動するように52行目「Me.ScrollBar1.SmallChange = 1」で最小移動量を1に設定します。

5-2-2.フォーム起動、戻り値設定

作業用シートに仕込んだマクロから呼び出されるのが図5-9です。今回サンプルファイルで言うと、図4-2の5行目、図4-3の11行目から呼び出されます。
引数として、セルの値を受け取ります。セル値は日付とは限らないためVariant型にしています。
  1. '========== ⇩(5) フォーム起動、戻り値設定 ============
  2. Public Function UFstart(D As Variant) As Variant
  3.  orgDate = D
  4.  If IsDate(D) Then
  5.   UFdate = DateSerial(Year(D), Month(D), 1)
  6.  Else
  7.   UFdate = DateSerial(Year(Date), Month(Date), 1)
  8.  End If
  9.  Call makeCal
  10.  Me.Show
  11.  UFstart = orgDate
  12. End Function
図5-9

まず60行目「orgDate = D」で、引数として受け取ったセル値を共通変数orgDateに代入し、保管しておきます。この変数は「ユーザーがカレンダーの日付をクリックした時のみ、その日付の値に書き換え」られます。

61行目「If IsDate(D) Then」で、セル値が日付か否かで、表示するカレンダーの年月を切り替えています。
まずセル値が日付の場合、62行目「UFdate = DateSerial(Year(D), Month(D), 1)」で、セル値の日付が含まれる「年月の初日」を共通変数UFdateに代入します。
日付では無い場合は、64行目「UFdate = DateSerial(Year(Date), Month(Date), 1)」で、今日を含む「年月の初日」を共通変数UFdateに代入します。

67行目「Call makeCal」では、図5-10を呼出し、Labelカレンダーを計算・作成します。その作成する基準の年月日(初日)がUFdateになります。
67行目が実行されれば、フォーム上にカレンダーが完成していますので、69行目「Me.Show」でフォームを起動します。

ユーザーがカレンダーを操作し「日付をクリック」するか、「右上×印をクリック」することでフォームは閉じられます。すると制御は70行目「UFstart = orgDate」に戻ってきます。
もし「日付をクリック」した時には、変数orgDateは「ユーザーが指定した日付」に置き換わっていますし、「右上×印をクリック」の場合は60行目で受け取ったセル値のままとなっています。70行目では、その変数orgDateの値をUFstart関数の戻り値に設定し、戻される先の作業用シートでは、その戻り値をセルに書き込むことになります。

5-2-3.カレンダー計算・作成

図5-9の67行目、図5-15の112行目、図5-16の124行目から呼び出される「カレンダーを計算し作成」するプロシージャが図5-10です。引数は個別に受け取らず、モジュール共通変数UFdate(作成するカレンダー年月の初日)を元に計算をします。
  1. '========== ⇩(6) カレンダー計算・作成 ============
  2. Private Sub makeCal()
  3.  Dim buf As String      'Labelに書き出すカレンダー文字列
  4.  Dim i As Integer      'カウンタ変数(カレンダーの日付)
  5.  Dim Calrow As Integer    'カレンダーの行位置(1~6)
  6.  Dim Calcol As Integer    'カレンダーの列位置(1~7)
  7.  Erase Calarray
  8.  Calrow = 1
  9.  Calcol = Weekday(UFdate)
  10.  buf = String(Calcol - 1, vbTab)
  11.  For i = 1 To Day(DateAdd("m", 1, UFdate) - 1)
  12.   buf = buf & Format(i, "00")
  13.   Calarray(Calrow, Calcol) = i
  14.   If Calcol = 7 Then
  15.    buf = buf & vbLf
  16.    Calrow = Calrow + 1
  17.    Calcol = 1
  18.   Else
  19.    buf = buf & vbTab
  20.    Calcol = Calcol + 1
  21.   End If
  22.  Next i
  23.  Me.Label1.Caption = buf
  24.  Me.Label3.Caption = Format(UFdate, "yyyy年mm月")
  25. End Sub
図5-10

このmakeCalプロシージャでは、カレンダー計算を行いながら、「Labelカレンダーの作成」と「配列カレンダーの作成」を同時におこなっています。
Labelカレンダーの方は、「日付である数字」の間に「TAB」または「改行(vbLf)」を混ぜながら文字列(変数buf)を作り、最後にLabel1のCaptionに変数bufを設定することで「6行×7列の表」の様に見えるカレンダーとしています。
一方配列カレンダーの方は、実際に「6行×7列の配列」の中に数値を埋め込んでいきますので、その位置制御のためにCalrow(78行目)・Calcol(79行目)が必要になってきます。

81行目「Erase Calarray」では、カレンダーを内部的に保持している配列Calarrayを全クリアします。
83行目「Calrow = 1」は、配列カレンダーの1行目にカーソル位置を合わせています。
84行目「Calcol = Weekday(UFdate)」は、配列カレンダーの書込み列位置を「表示カレンダー初日の曜日値」にしています。例えば初日が日曜であれば1に、火曜であれば3にします。
85行目「buf = String(Calcol - 1, vbTab)」では、図5-11のように、表示カレンダーの初日の位置まで移動するように「曜日値ー1」個のTABを入れます。例えば、図5-11では初日が金曜(曜日値=6)ですので、「6-1=5個」のTABを入れることで、初日の金曜日に列位置が移動することになります。
ラベルカレンダーの初日へTABで移動
図5-11

87行目「For i = 1 To Day(DateAdd("m", 1, UFdate) - 1)」では、カウンタ変数iを初日「1」から最終日「Day(DateAdd("m", 1, UFdate) - 1)」まで回しています。その月の最終日は、「翌月の0日目の日付」で求まりますので、DateAdd関数を用いて計算していますが、「Day(DateSerial(Year(UFdate),Month(UFdate)+1, 0))」でもOKです。

88行目「buf = buf & Format(i, "00")」では、LabelのCaptionに設定する文字列bufの後ろに日付を加えます。その際、日付を2桁表示にしています。もし2桁表示にしない場合、図5-12のように「1~9日は1桁」になります。
好き嫌いがあると思いますが、なんとなくバランス悪く感じたので、今回は2桁表示としています。
日付を2桁表示しない場合
図5-12

89行目「Calarray(Calrow, Calcol) = i」では、配列カレンダーCalarrayに日付を代入しています。Calarrayは図5-2の18行目でInteger型として宣言していますので、空欄部分には「ゼロ」が入ることになります。

91~98行目では、日付の後ろに「TABを入れて、右隣の曜日に移動」するか、「改行を入れて、一つ下の行に移動」するかを決めます。91行目「If Calcol = 7 Then」では、配列カレンダーの列位置が右端列(土曜日)の場合は92~94行目を実行します。
土曜の次は日曜ですので、次の行の先頭に移動する必要があります。ラベルカレンダーで言えば、図5-14のように「改行を入れる」ことになります。
92行目「buf = buf & vbLf」では、文字列bufの後ろに「改行」である「vbLf」を加えます。改行マークには、Windowsで一般的である「vbCrLf」が良く使われますが、図5-13のようにExcel専用の「vbLf」を含めて、Label内ではどれでも改行してくれます。
改行マーク違いでの出力文字列
図5-13

93行目「Calrow = Calrow + 1」では、配列カレンダーのカーソルを1つ下の行に移動させ、かつ94行目「Calcol = 1」で配列カレンダーの列位置を先頭列にします。

土曜日以外(日曜~金曜)の時には96~97行目を実行します。
96行目「buf = buf & vbTab」では、日付文字の後ろに「TAB」を加えます。また97行目「Calcol = Calcol + 1」で配列カレンダーの列位置を1つ右に移動します。

以上の工程で作られた「文字列 buf」は、102行目「Me.Label1.Caption = buf」で、Label1のCaptionに設定されます。Labelに貼り付けた文字列は、図5-14のようなイメージです。
ラベルカレンダー内の文字列
図5-14

103行目「Me.Label3.Caption = Format(UFdate, "yyyy年mm月")」では、表示カレンダーの初日であるUFstart値を「YYYY年MM月」というフォーマットでLabel3のCaptionに設定します。

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

カレンダー年月を変更させるために、スクロールバーを操作した時に呼び出されるのが図5-15です。
  1. '========== ⇩(7) スクロールバーでの移動 ============
  2. Private Sub ScrollBar1_Change()
  3.  If IsMoveSB = True Then Exit Sub
  4.  UFdate = DateAdd("m", Me.ScrollBar1.Value, UFdate)
  5.  Call makeCal
  6.  IsMoveSB = True
  7.   Me.ScrollBar1.Value = 0
  8.  IsMoveSB = False
  9. End Sub
図5-15

109行目「If IsMoveSB = True Then Exit Sub」で使用されている変数IsMoveSBは、マクロ側からスクロールバー値を変更している時に「Changeイベントを実行させないフラグ」です。このフラグはInitializeイベント(図5-4の47行目)およびこのイベントプロシージャの114行目でTrue(フラグをON)にしています。
フラグがTrueの場合は「Exit Sub」で、それ以降のコードをスルーします。

111行目「UFdate = DateAdd("m", Me.ScrollBar1.Value, UFdate)」では、移動したスクロールバーの値の分(月数)だけ、カレンダーの年月(UFdate)を変更します。
そして112行目「Call makeCal」では、111行目で再設定した年月(UFdate)で、カレンダを再表示します。

この段階では、スクロールバーのスクロールボックス(値を移動するツマミ部分)は中央からズレた位置になっているため、115行目「Me.ScrollBar1.Value = 0」で中立位置に戻します。この戻す時(=Value値を変更する時)にChangeイベントが発生してしまうため、114行目「IsMoveSB = True」でフラグを立て、再帰呼び出ししたChangeイベントをスルーさせます。

「今月ボタン」をクリックした時に呼び出されるのが図5-16です。
  1. '========== ⇩(8) 今月ボタンをクリック ============
  2. Private Sub CommandButton1_Click()
  3.  UFdate = DateSerial(Year(Date), Month(Date), 1)
  4.  Call makeCal
  5. End Sub
図5-16

123行目「UFdate = DateSerial(Year(Date), Month(Date), 1)」で、今日を含む年月の初日をUFdate(カレンダーの初日)に設定し、124行目「Call makeCal」でカレンダを再表示します。

5-2-5.カレンダーの日付をクリック

Label1のカレンダー本体の上でマウスをクリックすると、図5-17のMouseDownイベントが発生します。Labelのどこをクリックしたかを取得したいため、このMouseDownを使用しています。
渡される引数は4つありますが、今回使用するのは「X」「Y」のみです。ですので、マウスの左右どちらのボタンをクリックしても、またShiftを押しながらクリックしても、同様の動きをします。
  1. '========== ⇩(9) カレンダー上でマウスクリック ============
  2. Private Sub Label1_MouseDown(ByVal Button As Integer, ByVal Shift As Integer, _
  3.                 ByVal X As Single, ByVal Y As Single)
  4.  Dim Calrow As Integer    'カレンダーの行位置(1~6)
  5.  Dim Calcol As Integer    'カレンダーの列位置(1~7)
  6.  Calrow = Int(Y / Fsize) + 1
  7.  Calcol = Int((X + Fsize / 2) / TabWidth) + 1
  8.  If Not Calarray(Calrow, Calcol) = 0 Then
  9.   Call CalMark(Calrow, Calcol)
  10.   orgDate = UFdate + Calarray(Calrow, Calcol) - 1
  11.   Me.Hide
  12.  End If
  13. End Sub
図5-17

カレンダーが描かれているLabel1をXY座標的にみると、図5-18のような寸法で表す事が出来ます。
実際にカレンダーがあるのはLabel1の範囲ですが、Formとの横方向のスペース(Fsize/2)も加えて「カレンダー全面(細い赤色点線)」がカレンダー範囲と考えると、1日分のスペースは「半角4文字分の範囲の真ん中に日付の数字が2つ並び、その両隣に1文字分のスペースがある」と考えることが出来ます。
ラベルカレンダー内のXY座標
図5-18

そのカレンダー座標の上で、ユーザーがクリックした位置をXYの引数で受け取るので、その縦方向の位置(Calrow)は134行目「Calrow = Int(Y / Fsize) + 1」で表されることになり、また横方向(Calcol)は135行目「Calcol = Int((X + Fsize / 2) / TabWidth) + 1」となります。

そして、makeCalプロシージャ(図5-10)でカレンダーを作る際に、同時に配列カレンダー(Calarray)も作成しましたが、その配列は図5-18で示すカレンダーと同じ形です。
ですので、縦位置Calrow・横位置Calcolを頼りに配列Calarrayから値を取り出せば、「何日の部分をクリックしたか」が分かることになりますし、もし配列Calarray値が「ゼロ」だった場合は「日付では無い部分をクリックした」ことも分かります。

137行目「If Not Calarray(Calrow, Calcol) = 0 Then」では、クリックしたのが「ゼロ以外(=日付部分)」の場合に138~140行目を実行します。日付以外の部分をクリックしている場合は、そのままプロシージャを抜けますので何も起こらないことになります。
138行目「Call CalMark(Calrow, Calcol)」では、図5-19のCalMarkを呼出し、「ユーザーが何日をクリックしたか」を感覚的に再確認させるために、クリックした日付を一瞬だけ赤文字にしています。この動作が無いと、クリックしてすぐにフォームが消えてしまうため「自分がクリックした日付は合っていたのか?」「間違った日付をクリックしたのでは無いか?」という気持ちを持ってしまうと考え、一瞬だけ赤文字にしました。
139行目「orgDate = UFdate + Calarray(Calrow, Calcol) - 1」では、カレンダーの初日(UFdate)にクリックした日付を加えて「ユーザーが指定した日付」を作成し、共通変数orgDateに代入します。この変数orgDateには、起動時「既存のセル値」を入れていますので、クリックした日付に置き換えることになります。
140行目「Me.Hide」で、フォームを閉じ、制御をUFstartプロシージャ(図5-9)に戻します。

図5-17の138行目から呼び出される「クリックした日付を一瞬だけ赤文字にする」のが図5-19です。
引数として、配列カレンダーの行位置(Calrow)・列位置(Calcol)を受け取ります。
  1. '========== ⇩(10) クリックした日付を赤字 ============
  2. Private Sub CalMark(Calrow As Integer, Calcol As Integer)
  3.  Dim t As Single     '現在の時刻
  4.  With Me.Controls.Add("forms.label.1", "myLabel", True)
  5.   .Left = Me.Label1.Left + (Calcol - 1) * TabWidth
  6.   .Top = Me.Label1.Top + (Calrow - 1) * Fsize
  7.   .Width = Fsize + 2
  8.   .Height = Fsize + 2
  9.   .Font.Name = "MS ゴシック"
  10.   .Font.Size = Fsize
  11.   .ForeColor = RGB(255, 0, 0)
  12.   .Caption = Format(Calarray(Calrow, Calcol), "00")
  13.  End With
  14.  t = Timer()
  15.  Do While t + 0.3 > Timer()
  16.   DoEvents: DoEvents
  17.  Loop
  18.  Me.Controls.Remove ("myLabel")
  19. End Sub
図5-19

「一瞬だけ赤字にする」と言っても、Label内の一部の文字だけを赤くする事は出来ません。そこで、カレンダーの上に「新しい赤文字のLabelを作って重ねる」ことにしました。

149行目「With Me.Controls.Add("forms.label.1", "myLabel", True)」では、フォーム上に新しいコントロール(Label)を作ります。新しいLabelの名前は「myLabel」としています。

150行目「.Left = Me.Label1.Left + (Calcol - 1) * TabWidth」では、新Labelの横方向位置を計算します。フォーム上での位置になりますので、フォームとのズレ(Me.Label1.Left)を足しています。なお、図5-4の27行目で「Me.Label1.Left = Fsize / 2」としていますので、ズレ分は「Fsize / 2」としてもOKです。
151行目「.Top = Me.Label1.Top + (Calrow - 1) * Fsize」では、新Labelの縦方向位置を計算します。縦方向もフォーム上での位置ですので、Label1のフォーム上での位置「Me.Label1.Top」とLabel1内での日付位置「 (Calrow - 1) * Fsize」の合計値とします。

また新Labelの幅は、半角文字2個分ですが、ギリギリだと最後の文字が欠落(次の行に移動)してしまうため、152行目「.Width = Fsize + 2」と余裕分2ピクセルを加えています。
153行目「.Height = Fsize + 2」は、高さを指定していますが、幅と同様に2ポイントの余裕分を加えています。

154行目「.Font.Name = "MS ゴシック"」では、カレンダー本体と同様のフォントとします。なお通常のLabelのプロパティ設定であれば、図5-4の28行目のように「.Font = "MS ゴシック"」と「Name」を省略してもOKですが、マクロ側から作ったLabelの場合ではエラーが発生します。正確に「Font.Name」で設定する必要があります。

155行目「.Font.Size = Fsize」でフォントサイズをカレンダー本体と同じサイズにします。
156行目「.ForeColor = RGB(255, 0, 0)」で、フォントの色を赤色に設定します。
157行目「.Caption = Format(Calarray(Calrow, Calcol), "00")」で、新Labelにクリックした日付の文字列を、カレンダー本体と同じフォーマット(2桁表示)で設定します。

なお今回は、カレンダー本体設定(図5-4)でLabel1には「BorderStyle」を未設定(既定値:枠線を付けない)にしています。もしBorderStyleを付ける設定にすると、枠線の太さ分だけズレが生じてしまうので「カレンダー本体と新LabelのBorderStyle値は合わせる」必要があります。

ここまでで、クリックした日付が赤く(上から赤い文字のLabelを重ねているだけですが)なります。160~163行目のDo~Loopでは、赤くした状態を短い時間(今回は0.3秒間)だけ保持します。
160行目「t = Timer()」で、現在の時刻をミリ秒単位(実際にはSingle型の精度)で取得します。
161行目「Do While t + 0.3 > Timer()」では、160行目で取得した時刻を元に0.3秒間だけDo~Loopを回します。

160~163行目のDo~Loopを抜けたら(=赤文字にして0.3秒経過したら)、165行目「Me.Controls.Remove ("myLabel")」で、149行目で作成した新Labelを削除します。

5-2-6.カレンダー処理のキャンセル

カレンダーの右上×印をクリックして「カレンダーによる日付入力をキャンセル」した場合には、図5-20が呼び出されます。
  1. '========== ⇩(11) 右上×印をクリック ============
  2. Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
  3.  Cancel = True
  4.  Me.Hide
  5. End Sub
図5-20

QueryCloseイベントがそのまま実行されてしまうと、保存していた変数なども破棄されてしまいます。
そうなると、作業シート側から呼び出されたUFstart関数は「Emptyになった変数orgDate」を作業用シートに戻す事になってしまい、「既に記入されていた日付を消してしまう」ことになります。
ですので、170行目「Cancel = True」で「フォームの破棄をキャンセル」します。
その上で171行目「Me.Hide」でフォームを閉じます。

6.最後に

今回はフォーム上のラベルにカレンダーを表示させましたが、同様の方法で「ワークシート上にActiveXコントロールのラベルを置き、そのラベルにカレンダーを表示」し操作することも可能だと思います。ただし、表示年月移動用の機能も同時に表示させるためには、一工夫必要かもしれません。今後トライしてみたいと思います。

今回は「文字と文字はTABで離す」ようにしました。当初「スペース」や「_(アンダースコア)」などを試したのですが、「よりみち」でも説明した通りうまく行きませんでした。そのため、図2-4を見ても分かる通り「文字サイズが変わっても、カレンダーの横幅は不変」となってしまいました。
バランスとしては24ポイント当たりがカッコイイのですが、少し大きすぎる気がします。TABのポイント数の設定が変更できれば、と思うのですが、設定可能なのかも含めて分かりませんでした。

Labelの1行目と2行目の間はゼロポイントでは無く、フォントサイズ24の時には0.7ポイント位の隙間がありそうです。18ポイントの時にはほとんどゼロのようなので、サイズの大きなフォントの時のみ行間隙間を設けているのかもしれません。カレンダーに大きなフォントサイズを使う時には、多少調整する必要が出てくるかもしれません。


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