【完成品】エクセルVBAでカレンダーフォームを作成してみた
「他の人に配布するエクセルでカレンダーを使いたい…」という方のために、プログラマーの私が自作したカレンダーフォームを公開します。
このページを読んでいる方はすでにご存じかと思いますが、Microsoft標準のカレンダーは専用のDLLがいるみたいで、配布用のファイルには使いにくいのです。
ちなみに、当ブログで公開しているエクセル家計簿にもこのフォームを組み込んでいて、1万回以上はダウンロードされているので、品質と実績はけっこうある方です。
以下の順番で、できるだけこまかく説明していきます。
- カレンダーフォームの使い方
- カレンダーフォームの呼び出し方
- カレンダーフォームの仕組み
- VBA(frmCalendar)のダウンロード
Contents
カレンダーフォームの使い方
カレンダーの基本的な使い方としては、以下のGIF画像をご覧ください。
セルをクリックしてカレンダーを表示していますが、ボタンなどを含めて他のイベントでも表示することができます。
※矢印クリックで再生
実際に動かしてみたい方は、下記ページのエクセル家計簿をダウンロードして、「家計簿」というシートにある「日付」列をクリックしてみてください。
VBAを公開しているので、呼び出し方の確認やステップ実行なども可能です。
⇒【エクセル家計簿】貯金が得意なプログラマーがテンプレートを作ってみたカレンダーフォームの呼び出し方
セルのクリックでカレンダーを表示する場合は、WorksheetオブジェクトのSelectionChangeイベントを使います。
VBAエディター左部に表示されるSheet1などをクリックした後、上部のWorksheet_SelectionChangeを選ぶと、イベントハンドラーが自動生成されます。
ボタンなどでカレンダーを表示する場合は、標準モジュールなどお好きなところで大丈夫です。
カレンダーを呼び出すコードは、下記のサンプルを参考にしてください。
▼A1セルを押したときにカレンダーを表示する
A1セル以外のセルが押されたらカレンダーを非表示、A1セルが押されたらカレンダーを表示します。
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
If Target.Address <> "$A$1" Then
frmCalendar.Hide
Exit Sub
End If
Set frmCalendar.TargetRange = Target
frmCalendar.Initialize
frmCalendar.Show vbModeless
End Sub
▼テーブル(表)で特定の列のみカレンダーを表示する
最初のIf文は、複数のセルが同時選択されたときに、カレンダー表示をキャンセルするのが目的です。
複数セルに日付を同時入力したい場合は、このIf文は不要です。
ふたつ目のIf文は、テーブルの日付列以外が選択された場合のキャンセル処理。
三つ目のIf文は、テーブルの範囲外にある行が選択されたときのキャンセル処理です。
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
'範囲選択を無視
If Target.Count > 1 Then
frmCalendar.Hide
Exit Sub
End If
'日付の入力欄以外を無視
If Target.Column <> Hoge Then
frmCalendar.Hide
Exit Sub
End If
If Target.row <= Fuga Then
frmCalendar.Hide
Exit Sub
End If
Set frmCalendar.TargetRange = Target
frmCalendar.Initialize
frmCalendar.Show vbModeless
End Sub
とりあえずカレンダーを表示させるには、ふたつ目のサンプル、20~22行目をコピペしてもらえれば動きます。
日付を入力したいセルをRangeオブジェクトとして取得し、カレンダー(frmCalendar)のTargetRangeという変数にセットします。
frmCalendarを閉じるときは、UnloadではなくHideを使ってください。
詳しいことは後でお話ししますが、カレンダーを再表示したときに表示位置を復元するためです。
カレンダーフォームの仕組み
カレンダーがどのように実装されているかについても、解説したいと思います。
中身をしっかり吟味したい方や自分なりにカスタマイズしたい方は、参考にしてみてください。
定数と変数の宣言~初期化
定数と変数の宣言、初期化のコードです。
Option Explicit
Private Const COLOR_NORMAL = &HE0E0E0
Private Const COLOR_TODAY = &H99FFFF
Public TargetRange As Range
Public Sub Initialize()
Dim dt As Date
If IsDate(TargetRange.value) = False Then
dt = Date
Else
dt = TargetRange.value
End If
SetCalendar year(dt), month(dt)
End Sub
定数のCOLOR_NORMAL と COLOR_TODAYは、日付ボタン(1~31日)の配色に使います。
今日の日付は黄色、その他は灰色となっています。
TargetRangeは、カレンダーを表示する前に呼び出し元でセットするRangeオブジェクトです。
最終的には、日付ボタンを押したタイミングでこのTargetRangeに日付をセットし、セルに反映…となります。
Initializeは、名前のとおり初期化のための関数です。
TargetRangeになにも入力されていないときは今月、すでに入力されているときはその月をカレンダーの初期表示とします。
前月・次月ボタン
左矢印ボタン、右矢印ボタンをクリックすると、前月または次月のカレンダーを表示します。
コードはこんな感じになっています。
Private Sub lblPreMonth_Click()
MovePreMonth
End Sub
Private Sub lblPreMonth_DblClick(ByVal Cancel As MSForms.ReturnBoolean)
MovePreMonth
End Sub
Private Sub MovePreMonth()
Dim dt As Date
dt = DateSerial(lblYear.Caption, lblMonth.Caption - 1, 1)
SetCalendar year(dt), month(dt)
End Sub
Private Sub lblNextMonth_Click()
MoveNextMonth
End Sub
Private Sub lblNextMonth_DblClick(ByVal Cancel As MSForms.ReturnBoolean)
MoveNextMonth
End Sub
Private Sub MoveNextMonth()
Dim dt As Date
dt = DateSerial(lblYear.Caption, lblMonth.Caption + 1, 1)
SetCalendar year(dt), month(dt)
End Sub
前月ボタン(lblPreMonth)、次月ボタン(lblNextMonth)に、それぞれClickとDblClickのイベントハンドラーを実装しています。
ダブルクリックすると、Clickイベントが2回ではなく、ClickイベントとDblClickイベントが1回ずつ発生します。
そのため、ダブルクリックしたときに2ヵ月先のカレンダーを表示するためには、DblClickのイベントハンドラーが必要となります。
もしわかりにくければ、DblClickのイベントハンドラーを消去して検証してみてください。
ボタンを連打してもなかなか月が変わらないので、イラっとすると思います。
MovePreMonth、MoveNextMonthに登場するlblYearとlblMonthは、カレンダー上部に表示されている年月のラベルです。
前月ボタンと次月ボタンがクリックされると、ここに表示されている年月の前月または次月をカレンダー表示の共通処理に渡します。
カレンダーの描画
初期表示、前月次月ボタンがクリックされたときに呼ばれる、カレンダーを描画するための共通処理です。
Private Sub SetCalendar(ByVal year As Integer, ByVal month As Integer)
'年月を表示
lblYear.Caption = year
lblMonth.Caption = month
Dim i As Integer
Dim lblDay As Object
'カレンダーを初期化
For i = 1 To 37
Set lblDay = Me.Controls("lblDay" & i)
lblDay.Caption = ""
lblDay.BackColor = COLOR_NORMAL
Next
'始めの日
Dim firstDate As Date
firstDate = DateSerial(year, month, 1)
'曜日によって位置をずらす
Dim position As Integer
position = weekday(firstDate) - 1
'終わりの日
Dim endDay As Integer
endDay = day(DateSerial(year, month + 1, 0))
For i = 1 To endDay
Set lblDay = Me.Controls("lblDay" & (i + position))
lblDay.Caption = i
If DateSerial(year, month, i) = Date Then
lblDay.BackColor = COLOR_TODAY
End If
Next
End Sub
最初に、カレンダー上部にある年月のラベルに表示対象の年月をセットします。
その後は、日付ボタン(1~31日)の値と背景色をいったんクリアして、日付を埋めるループに入ります。
日付ボタンの中に今日のものがあれば、その背景色は定数のCOLOR_TODAY(黄色)となります。
日付ボタン(1~31日)
カレンダーの中央にある日付ボタンを押すと、その日付を対象のセル(TargetRange)に出力します。
コードはこんな感じになっています。
Private Sub OutputDate(ByVal day As String)
If day = "" Then
Exit Sub
End If
TargetRange.value = DateSerial(lblYear.Caption, lblMonth.Caption, day)
Me.Hide
End Sub
Private Sub lblDay1_Click()
OutputDate lblDay1.Caption
End Sub
Private Sub lblDay2_Click()
OutputDate lblDay2.Caption
End Sub
'以下省略 lblDay31_Click()まで続く
lblDay1_Click、lblDay2_Click…というように、ボタンの数だけクリックイベントを拾うイベントハンドラーがあって、それぞれが共通処理のOutputDateを呼びます。
共通処理では、カレンダー上部の年月ラベルと日付ボタンのラベルから日付をつくり、入力対象のセル(TargetRange)にセットします。
表示位置の復元
カレンダーを2回、3回と再表示したときに、前と同じ位置に表示させるため、ちょっとだけ工夫しています。
コードはこんな感じ。
Private Sub UserForm_Layout()
'フォームの位置が変わった場合は、
'再表示時にその位置を復元する
StartUpPosition = 0
End Sub
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
'表示位置がリセットされないようにする
Cancel = True
Me.Hide
End Sub
カレンダーフォーム(frmCalendar)のStartUpPositionプロパティは、「1 - オーナーフォームの中央」となっていて、初期表示ではエクセルの中央に表示されます。
カレンダーを好きなところに動かすと、UserForm_Layoutが実行されて、同プロパティを「0 - 手動」に変更します。
これにより、カレンダーを表示しなおした際、エクセルの中央ではなく前回と同じ位置に表示されるようになります。
また、画面右上の閉じるボタン(×ボタン)が押された場合は、UserForm_QueryCloseが実行されます。
そのまま閉じてしまうと、StartUpPositionが初期値の「1 - オーナーフォームの中央」にもどり、表示位置を復元することができません。
そのため、画面を"閉じる"のはキャンセルして、“隠す"ようにしています。
VBA(frmCalendar)のダウンロード
カレンダーフォームは、以下のリンクをクリックするとダウンロードできます。
⇒カレンダー(frmCalendar)をダウンロードZIPファイルを解凍したら、VBAエディターにある「ファイルのインポート」からカレンダーフォームを取り込んでください。