2011年12月25日日曜日

[ExcelVBA] 1ヶ月のカレンダーを表示する


■お題
1ヶ月表示のカレンダーを作ってみてください。

■解答例
今回は、修正のしやすさを考慮してモジュールをいくつかに分けて(整理しながら)書いてみようと思います。
今回、
MainModule…メインのコードの流れを書く
SubModule…メインのコードから呼ばれる関数や、カレンダーマクロ特有の関数を書く
CommonModule…他のマクロからも活用する機会がありそうな関数
SetModule…設定変数が書いてある変数
SandBoxModule…わからないことや書いたコードのテストを書く
このように分けて書く事であとで修正する際にどこを見ればいいか判断しやすい状態にしておく。

ひとまず、カレンダーの完成図をイメージしてみる。

(※列の幅は、2.5になっています。)


まずは、カレンダーの基準セル(カレンダーの左上の位置)を決めその位置から
相対的にパーツを配置することで変更が簡単にできるようにしておく。

モジュール名:SetModule…(分けてる目的)設定変数をまとめておく。
Public Const BASE_CELL_ROW = 2
Public Const BASE_CELL_COL = 2

1行目:カレンダーの左上のセルで行の情報を格納
2行目:カレンダーの左上のセルで列の情報を格納
どの位置に出力するかは、この変数の値をかえればよいと。


次に、日付を表示してみます。
基準(カレンダーの左上)と同じ行で、2列隣の場所に表示することにします。
モジュール名:SubModule…(分けてる目的)メインのコードから呼ばれる関数や、カレンダーマクロ特有の関数を書く
Sub Say_Today()
    Dim r As Long, c As Integer
    r = BASE_CELL_ROW
    c = BASE_CELL_COL
    Cells(r, c + 2) = Date
End Sub

2行目:行を格納変数、列を格納する変数を用意。行は、65536行まであることからLong型
3行目:基準セルの行を代入
4行目:基準セルの列を代入
5行目:基準セルと同じ行、2つ列を移動したとこに日付を表示


ここで、考察。
今後、関数を書くたびに行と列を格納する変数を用意し、
それぞれ毎回初期化するたびに、お決まりの2行を書くことになるので
ちょっとでも楽するために1行で代入が済ませれるよう工夫してみる。


上記コードがこうなる。
モジュール名:SetModule…(分けてる目的)設定変数をまとめておく
Sub InitBaseCell(ByRef r As Long, ByRef c As Integer)
    r = BASE_CELL_ROW
    c = BASE_CELL_COL
End Sub

Sub Say_Today()
    Dim r As Long, c As Integer
    Call InitBaseCell(r, c)
    Cells(r, c + 2) = Date
End Sub

InitBaseCellという、基準セル位置を初期化する用の関数を用意しました。
これは、参照渡しで値を渡すため、列と行の順番で渡してあげればそちらで代入することになるので
利用する時は、Call InitBaseCellの一行で済ませれます。


想定した挙動になっているか確認
モジュール名:SandBoxModule(分けてる目的)わからないことや書いたコードのテストを書く
Sub t1()
    Dim r As Long, c As Integer
    Call InitBaseCell(r, c)
    Debug.Print "Row:" & r & " Col:" & c
End Sub

特に問題ないですね。



次に、日~土の曜日を表示する関数を追加します。
モジュール名:SetModule…(分けてる目的)設定変数をまとめておく
Sub Say_DayStr()
    Dim r As Long, c As Integer
    Call InitBaseCell(r, c)
    r = r + 1
    
    Cells(r, c) = "日"
    Cells(r, c + 1) = "月"
    Cells(r, c + 2) = "火"
    Cells(r, c + 3) = "水"
    Cells(r, c + 4) = "木"
    Cells(r, c + 5) = "金"
    Cells(r, c + 6) = "土"
End Sub

2行目:行を格納変数、列を格納する変数を用意。行は、65536行まであることからLong型
3行目:行と列変数に基準セルの行と列の値を代入
4行目:基準セルの一段下に表示したいので1加算しておく
6行~12行目:日~土を代入。


ここまでで、ちゃんと表示されるかテストしてみます。
モジュール名:SandBoxModule(分けてる目的)わからないことや書いたコードのテストを書く

Sub t2()
    'Show_Today関数のテスト
    Call Say_Today
    Call Say_DayStr
End Sub

こうなった。

(※列の幅は、2.5になっています。)

日付部分は、セルの範囲が狭いのできちんと表示されてないですけど
セルの結合等で適切なサイズのセルを用意すれば問題ないですね。
見た目の調整については、いつやっても問題ないことに関しては極力最後かつ1箇所の関数にまとめておきます。
修正する際にあちこちにちらばっているとさがすのが大変だからです。



次に、日にちを表示する関数を追加してみます。1日からその月の最終日をFor文でぐるぐる回しながら
表示していけばよさそうですね。ここで最終日を取得する関数も作ることにします。この関数は
他のマクロでも使えそうなので、CommonModuleにまとめておきます。
では、コードはこんな感じ。
モジュール名:SubModule…(分けてる目的)メインのコードから呼ばれる関数や、カレンダーマクロ特有の関数を書く
Sub Say_Day()
    Dim r As Long, c As Integer
    Call InitBaseCell(r, c)
    
    Dim d As Integer
    r = BASE_CELL_ROW + 2
    For d = 1 To LastDay()
        c = BASE_CELL_COL + Weekday(DateSerial(Year(Date), Month(Date), d)) - 1
        Cells(r, c) = d
        If Weekday(DateSerial(Year(Date), Month(Date), d)) = 7 Then r = r + 1
    Next
End Sub

モジュール名:SubModule内
2行目:行を格納変数、列を格納する変数を用意。行は、65536行まであることからLong型
3行目:行と列変数に基準セルの行と列の値を代入
5行目:For文で使う変数d(日にち部分を格納)を宣言
6行目:日にちは、基準セル(カレンダーの左上)より2つ下の行にかくことにします
7行目:For文 初期値は、日付なので1日。TOは、その月の最後の日付。LastDayという関数を別途作る。

先にLastDay関数について
関数の目的として、他のマクロから利用しやすくするために
ある日付をあたえるとその月の最後の日にちを返すということにします。
引数は、省略可能で省略すると今月の最後の日付を返すことにします。
最終日を求めるのに、まず次の月の1日の日付を調べ、
その前日の日付を割り出すことで最終日を求めることにします。

モジュール名:CommonModule…(分けてる目的)他のマクロでも使えそうな関数
Function LastDay(Optional UserDate As Date) As Integer
    Dim SetDate As Date
    
    If UserDate Then
        SetDate = UserDate
    Else
        SetDate = Date
    End If
    
    Dim NextMonthFirstDate As Date '次の月の1日の日付を格納
    NextMonthFirstDate = DateSerial(Year(SetDate), Month(SetDate) + 1, 1)
   
    Dim LastDate As Date 'SetDate日付の最後の日付を格納
    LastDate = DateAdd("d", -1, NextMonthFirstDate)
    
    LastDay = Day(LastDate)
End Function

モジュール名:CommonModule内
1行目:引数は省略可能ということでOptionalキーワードをつけてます。
2行目:SetDateという変数を宣言します。(この変数の日付の最後の日を返す)
4行目:If文でオプション用の引数に値があるかチェック
5行目:値がある時は、引数をSetDateに格納
6行目:引数がない時
7行目:今日の日付を格納
10行目:NextMonthFirstDateを宣言。
11行目:DateSerial関数を使って、setDateに入っている日付の次の月の1日の日付を代入する
13行目:SetDateに格納されている日付の最後の日付を格納する変数を宣言
14行目:次の月の1日の日付(NextMonthFirstDate)からDateAdd関数で1日戻ることで目的の最終日を求める
16行目:最終的に、Day関数で日にちを取り出して返す


じゃ、ちゃんと動くかテストしてみます。
モジュール名:SandBoxModule(分けてる目的)わからないことや書いたコードのテストを書く
Sub t5()
    Debug.Print LastDay()
    Debug.Print LastDay(CDate("2010/2/10"))
    Debug.Print ""
End Sub

ちゃんと動いていますね。


SetModuleのFor文に戻ります。
<再掲>
モジュール名:SubModule…(分けてる目的)メインのコードから呼ばれる関数や、カレンダーマクロ特有の関数を書く
Sub Say_Day()
    Dim r As Long, c As Integer
    Call InitBaseCell(r, c)
    
    Dim d As Integer
    r = BASE_CELL_ROW + 2
    For d = 1 To LastDay()
        c = BASE_CELL_COL + Weekday(DateSerial(Year(Date), Month(Date), d)) - 1
        Cells(r, c) = d
        If Weekday(DateSerial(Year(Date), Month(Date), d)) = 7 Then r = r + 1
    Next
End Sub

改めてFor文の中身を解説します。
行と列の特定方法は、列は、Weekday関数を使うことで1~7の値を返してくれるので
基準セルの列 + Weekday関数の返す値(1~7) - 1(補正値)
例えば、日曜日はWeekday関数で1を返すので基準セルと同じ列に表示するには、
Weekday関数の返す値から1ひかないと合わなくなります。
補正値と書いているのは、そういうことです。

行に関しては、Weekday関数の値を監視して土曜日だったら、日付表示後に行の変数の値を1つ増やせばいいですね。
8行目:列を求める
9行目:日にちを表示
10行目:表示した日が土曜日だったら、行の変数の値を1つ加算して次の行へ移動する
11行目:繰り返し


ここらへんで、カレンダーを表示するための関数としてmainという
名前の関数を用意して今までのコードを実行してみます。
モジュール名:MainModule…(分けてる目的)SubModuleに書いた関数を組み合わせカレンダーを作成する
Sub main()
    Call Say_Today
    Call Say_DayStr
    Call Say_Day
End Sub

シンプルで見通しがよいですね。
2行目:今日の日付を表示
3行目:曜日(日~土)を表示
4行目:1ヶ月文の日にちを表示


(※列の幅は、2.5になっています。)

データを書き込む部分は、これでおしまいですね。
あとは、見た目部分を調整していきます。
見た目は、いつ設定しても問題ないものについては最後に一つの関数内にまとめることにします。
モジュール名:SubModule…(分けてる目的)メインのコードから呼ばれる関数や、カレンダーマクロ特有の関数を書く
Sub View()
    Dim BaseR As Long, BaseC As Integer
    Call InitBaseCell(BaseR, BaseC)
    
    '日付部分処理
    Range(Cells(BaseR, BaseC + 2), Cells(BaseR, BaseC + 4)).Merge
    With Cells(BaseR, BaseC + 2)
        .Font.Size = 8
        .Font.Bold = True
        .HorizontalAlignment = xlCenter
    End With
    
    '曜日部分処理
    Range(Cells(BaseR + 1, BaseC), Cells(BaseR + 1, BaseC + 6)).HorizontalAlignment = xlCenter

    '土曜日を青色
    Range(Cells(BaseR + 1, BaseC + 6), Cells(BaseR + 6, BaseC + 6)).Font.ColorIndex = 5
    
    '日曜日を赤色
    Range(Cells(BaseR + 1, BaseC), Cells(CalendarEndRow(), BaseC)).Font.ColorIndex = 3

    '全体に関する設定
    With Range(Cells(BaseR + 1, BaseC), Cells(CalendarEndRow(), BaseC + 6))
        .Borders(xlEdgeLeft).LineStyle = xlContinuous
        .Borders.ColorIndex = 56
    End With
    
    With Range(Cells(BaseR, BaseC), Cells(CalendarEndRow(), BaseC + 6))
        .Interior.ColorIndex = 19
        .Font.Bold = True
        .Borders(xlEdgeLeft).Weight = xlMedium
        .Borders(xlEdgeLeft).ColorIndex = 56
        .Borders(xlEdgeBottom).Weight = xlMedium
        .Borders(xlEdgeBottom).ColorIndex = 56
        .Borders(xlEdgeRight).Weight = xlMedium
        .Borders(xlEdgeRight).ColorIndex = 56
        .Borders(xlEdgeTop).Weight = xlMedium
        .Borders(xlEdgeTop).ColorIndex = 56
    End With
    
    '本日のセルの色を変える
    Dim r As Integer
    r = 2
    Do
        If Cells(BaseR + r, BaseC + Weekday(Date) - 1) = Day(Date) Then
            Cells(BaseR + r, BaseC + Weekday(Date) - 1).Interior.ColorIndex = 35
            Exit Do
        End If
        r = r + 1
    Loop
End Sub

表示部分なので、ざくざく軽く説明していきます。
ここらへんは、マクロの記録で調べつつ余分なとこは省いたコードをのっけていってますね。
2行目:今まで他の関数ではrとcという変数を用意して作業してたのですがrとcはそれ自身の値を
書き換えて使っていたので、書き換えずに使うという意図を含めてBaseRとBaseCという変数にしました。
3行目:行と列変数に基準セルの行と列の値を代入

6行目~11行目
・日付部分のセル範囲がせまくて##になっているのでD~F列を結合して広くします。
・文字サイズは、8に変更
・太文字フォントに変更

14行目:曜日のセルをセンタリングします
17行目:土曜日の列を青色文字へ変更
20行目:日曜日の列を赤色文字へ変更

23行目~26行目:曜日より下の行と列に罫線をひきます。細線です
28行目~39行目:
・カレンダー全体に対し背景色を薄い黄色に変更
・太文字へ変更
・カレンダーの四辺を太線罫線で表示

42行目~50行目:本日のセルの色を変える
列は、Weekday関数と基準セル列があればわかりあとはDoループで今日の
日にちと一致するセルを探し見つかり次第セルの色を変えてます


上記コード内で、カレンダーの最後の行を取得する関数(CalendarEndRow)を作って使ってます。
20、23、28行目です。
モジュール名:SubModule…(分けてる目的)メインのコードから呼ばれる関数や、カレンダーマクロ特有の関数を書く
Function CalendarEndRow() As Long
    Dim r As Long, c As Integer
    Call InitBaseCell(r, c)
    
    Dim UsedCell As Integer
    UsedCell = LastDay() + Weekday(DateSerial(Year(Date), Month(Date), 1)) - 1
    r = BASE_CELL_ROW + 1 + Int(UsedCell / 7)
    If 0 < (UsedCell / 7 - Int(UsedCell / 7)) Then r = r + 1
    CalendarEndRow = r
End Function
main関数にView関数の呼び出しを追加して実行してみます。 モジュール名:MainModule…(分けてる目的)SubModuleに書いた関数を組み合わせカレンダーを作成する
Sub main()
    Call Say_Today
    Call Say_DayStr
    Call Say_Day
    Call View
End Sub
(※列の幅は、2.5になっています。) 最後の仕上げ。 月をまたぐと始まりの日の曜日や最終日がかわるので表示が残っているとおかしなことになります。 加えて、当日のセルは緑色に変更しているので毎日緑のセルがふえていくことになってしまうので 最初に初期化するコードを挟んでおきます。 基準セル位置から8行7列を削除してます。 モジュール名:SubModule…(分けてる目的)メインのコードから呼ばれる関数や、カレンダーマクロ特有の関数を書く
Sub init()
    Dim r As Long, c As Integer
    Call InitBaseCell(r, c)
    Range(Cells(r, c), Cells(r + 7, c + 8)).Clear
End Sub
そして、main関数にこの関数を追加すると完成です。 モジュール名:MainModule…(分けてる目的)SubModuleに書いた関数を組み合わせカレンダーを作成する
Sub main()
    Call init
    Call Say_Today
    Call Say_DayStr
    Call Say_Day
    Call View
End Sub
最後に全コードを改めて掲載しておきます。 ◆モジュール名:SetModule
Option Explicit

Public Const BASE_CELL_ROW = 2
Public Const BASE_CELL_COL = 2
◆モジュール名:MainModule
Option Explicit

Sub main()
    Call init
    Call Say_Today
    Call Say_DayStr
    Call Say_Day
    Call View
End Sub
◆モジュール名:SubMocule
Option Explicit

Sub Say_Today()
    Dim r As Long, c As Integer
    Call InitBaseCell(r, c)
    Cells(r, c + 2) = Date
End Sub

Sub Say_DayStr()
    Dim r As Long, c As Integer
    Call InitBaseCell(r, c)
    r = r + 1
    
    Cells(r, c) = "日"
    Cells(r, c + 1) = "月"
    Cells(r, c + 2) = "火"
    Cells(r, c + 3) = "水"
    Cells(r, c + 4) = "木"
    Cells(r, c + 5) = "金"
    Cells(r, c + 6) = "土"
End Sub

Sub Say_Day()
    Dim r As Long, c As Integer
    Call InitBaseCell(r, c)
    
    Dim d As Integer
    r = BASE_CELL_ROW + 2
    For d = 1 To LastDay()
        c = BASE_CELL_COL + Weekday(DateSerial(Year(Date), Month(Date), d)) - 1
        Cells(r, c) = d
        If Weekday(DateSerial(Year(Date), Month(Date), d)) = 7 Then r = r + 1
    Next
End Sub

Sub View()
    Dim BaseR As Long, BaseC As Integer
    Call InitBaseCell(BaseR, BaseC)
    
    '日付部分処理
    Range(Cells(BaseR, BaseC + 2), Cells(BaseR, BaseC + 4)).Merge
    With Cells(BaseR, BaseC + 2)
        .Font.Size = 8
        .Font.Bold = True
        .HorizontalAlignment = xlCenter
    End With
    
    '曜日部分処理
    Range(Cells(BaseR + 1, BaseC), Cells(BaseR + 1, BaseC + 6)).HorizontalAlignment = xlCenter

    '土曜日を青色
    Range(Cells(BaseR + 1, BaseC + 6), Cells(BaseR + 6, BaseC + 6)).Font.ColorIndex = 5
    
    '日曜日を赤色
    Range(Cells(BaseR + 1, BaseC), Cells(CalendarEndRow(), BaseC)).Font.ColorIndex = 3

    '全体設定
    '背景色を設定
    With Range(Cells(BaseR + 1, BaseC), Cells(CalendarEndRow(), BaseC + 6))
        .Borders(xlEdgeLeft).LineStyle = xlContinuous
        .Borders.ColorIndex = 56
    End With
    
    With Range(Cells(BaseR, BaseC), Cells(CalendarEndRow(), BaseC + 6))
        .Interior.ColorIndex = 19
        .Font.Bold = True
        .Borders(xlEdgeLeft).Weight = xlMedium
        .Borders(xlEdgeLeft).ColorIndex = 56
        .Borders(xlEdgeBottom).Weight = xlMedium
        .Borders(xlEdgeBottom).ColorIndex = 56
        .Borders(xlEdgeRight).Weight = xlMedium
        .Borders(xlEdgeRight).ColorIndex = 56
        .Borders(xlEdgeTop).Weight = xlMedium
        .Borders(xlEdgeTop).ColorIndex = 56
    End With
    
    '本日のセルの色を返る
    Dim r As Integer
    r = 2
    Do
        Debug.Print Cells(BaseR + r, BaseC + Weekday(Date) - 1)
        If Cells(BaseR + r, BaseC + Weekday(Date) - 1) = Day(Date) Then
            Cells(BaseR + r, BaseC + Weekday(Date) - 1).Interior.ColorIndex = 35
            Exit Do
        End If
        r = r + 1
    Loop
End Sub

Sub init()
    Dim r As Long, c As Integer
    Call InitBaseCell(r, c)
    Range(Cells(r, c), Cells(r + 7, c + 8)).Clear
End Sub

Function CalendarEndRow() As Long
    Dim r As Long, c As Integer
    Call InitBaseCell(r, c)
    
    Dim UsedCell As Integer
    UsedCell = LastDay() + Weekday(DateSerial(Year(Date), Month(Date), 1)) - 1
    r = BASE_CELL_ROW + 1 + Int(UsedCell / 7)
    If 0 < (UsedCell / 7 - Int(UsedCell / 7)) Then r = r + 1
    CalendarEndRow = r
End Function

Sub InitBaseCell(ByRef r As Long, ByRef c As Integer)
    r = BASE_CELL_ROW
    c = BASE_CELL_COL
End Sub
◆モジュール名:CommonModule
Option Explicit

Function LastDay(Optional UserDate As Date) As Integer
    Dim SetDate As Date
    
    If UserDate Then
        SetDate = UserDate
    Else
        SetDate = Date
    End If
    
    Dim NextMonthFirstDate As Date '来月1日の日付
    NextMonthFirstDate = DateSerial(Year(SetDate), Month(SetDate) + 1, 1)
   
    Dim LastDate As Date '今月最後の日付
    LastDate = DateAdd("d", -1, NextMonthFirstDate)
    
    LastDay = Day(LastDate)
End Function
◆モジュール名:SandBox
Option Explicit

Sub t1()
    Dim r As Long, c As Integer
    Call InitBaseCell(r, c)
    Debug.Print "Row:" & r & " Col:" & c
End Sub

Sub t2()
    'Show_Today関数のテスト
    Call Say_Today
    Call Say_DayStr
End Sub

Sub t3()
    Dim yyyymmdd As Date
    yyyymmdd = DateSerial(Year(Date), Month(Date), 1)
    Debug.Print yyyymmdd
    Debug.Print Weekday(yyyymmdd) '1=>日,7=>土
    Debug.Print ""
End Sub

Sub t4()
    Dim today As Date
    today = Date
    Debug.Print "本日の日付  :" & today
    
    Dim NextMonthFirstDate As Date
    NextMonthFirstDate = DateSerial(Year(Date), Month(Date) + 1, 1)
    Debug.Print "来月1日の日付:" & NextMonthFirstDate
    
    Dim LastDate As Date
    LastDate = DateAdd("d", -1, NextMonthFirstDate)
    Debug.Print "今月最後の日付:" & LastDate
    
    Dim LastDay As Integer
    LastDay = Day(LastDate)
    Debug.Print "今月の最後の日:" & LastDay
    Debug.Print ""
End Sub

Sub t5()
    Debug.Print LastDay()
    Debug.Print LastDay(CDate("2010/2/10"))
    Debug.Print ""
End Sub

Sub t6()
    Static num As Integer
    num = num + 1
    Debug.Print num
End Sub

0 件のコメント: