見出し画像

個サ作 #16 カレンダーLv.6 後編

こんにちは。

前回はあみだくじの演習をクリアし、カレンダーLv.6に入りました。二重ループの説明をしたところで終わりましたね。

今回は月間カレンダーの本格的な実装に入っていきましょう。

完成させます。


カレンダーLv.6 後編

ループの開始終了を定義しよう

下図が完成形なのですが、これを見てループの定義を推測してもらいたいなと、思います。変数 i は横方向(行)、変数 j は縦方向(縦)です。

完成形の状態

まず、行で見ると最も最初に値があるのは4行目ですね(年月情報を除いて)。「1日(土)」という文字がH列の4行目にあります。ですので、変数 i の開始番号は4でいいでしょう。

では終了値はいくつでしょう。最後に枠があるのは15行目です。ですが、これはあとからソースを組む段階で説明するのですが、2行ずつをセットで処理していきます。つまり・・・

行の処理は2行単位で行いますよ、という説明

こんな具合に、最後の1周を処理するとき、変数 i の値は14であればOKです。14行目であれば、14,15行目をセットで処理していく、という動きをします。

ここまでの話をソースコードに落とし込むと・・・

    '行ループ
    For i = 4 To 14 Step 2
    
    Next

このようになります。4から始まり14で終わる、その間2ずつインクリメントする、というループ処理の動きです。Stepを使ったのは今回が初めてですね。これで変数 i の値は4, 6, 8, 10, 12, 14という風に遷移します。

それでは列のループを見てみましょう。

列は1列ずつ左から右に進む。B列からH列まで。

こちらは変数 i よりシンプルでB列から始まりH列まで終わるのみです。列間の遷移も1マスだけですね。B列は2列目、H列は8列目なので、先ほどの行ループと併せると以下のようなソースコードになります。書いてください。

    '行ループ
    For i = 4 To 14 Step 2
        '列ループ
        For j = 2 To 8
        
        Next
    Next
赤枠のソースコードを反映してね

はい、ではためしに二重ループの中に以下のソースを書いてください。

            Debug.Print Cells(i, j).Address
赤枠のソースコードを反映してね

これはDebug.print。今までも何度も使ってきたようにイミディエイトウィンドウに出力するだけのソースなので、実行しても画面上の変化はありません。

ポイントはCells(i, j)に続く「.Addressアドレス」の部分です。このプロパティはセルの所在地を教えてくれます。実行すると・・・

今のソースコードを実行する様子

「$B$4」から「$H$14」までが出力されました。列を示す数値部分は2刻みです。Step 2と書いてますからね。

この矢印が通っている部分が実際にCells(i, j).Addressが示してくれるセルの場所です。なので、これらのセルを通っていく中で年月情報によって出す値を変えたりスキップしたりするわけですね。

さっき書いてもらった

            Debug.Print Cells(i, j).Address

このソースは消して置いてください。使わないのでね。


では、処理を書いていきましょう。ただこれまでと違って二重ループで卓上カレンダーによく見る形式で作りますから、実装する要素が盛りだくさんです。

  • 「1日」の出力列の設定

  • 日付出力可否の判定(主に29,30,31日)

  • 日付に対する曜日情報の付与

  • 曜日に応じたセルの装飾(文字色、背景色)

  • 行幅、列幅の調整

これらですね。今回初めてやるもの、これまでやってきたことをそのまま流用できるもの、応用して使うもの、様々です。


「1日」の出力列の設定

では、項タイトルの「1日」の出力列の設定からいきましょう。

まず前提として変数 i は行方向のループ(上から下へ)、変数 j は列方向のループ(左から右へ)を行います。

つまり、変数 i は週を担当し、変数 j は日を担当します。

そして、ある月の1日目は当然、日曜であることも月曜であることも火曜であることも・・・土曜であることも、7通りあります。

出力開始列がその年月の1日になるようにしたいです。そして出力開始列を管理する変数は既に宣言しています。そして前回やったcalendarInit関数の中で変数startColumnに値を設定しましたね。

これがまさしく指定された年月のカレンダー出力の1日目の列を示す値をもっています。と、いうことは?

日を担当する変数 j に変数startColumnの値を設定してやればよいのでは?という想像が働きます。

ここまでの解説を頭に入れた上で、まずはなんの制御もなしに日付を出力してみましょう。

次のソースを二重ループの中に書いてください。

            Cells(i, j).Value = day & "日"
            Application.Wait [Now() + "0:00:00.001"]
            day = day + 1
赤枠のソースコードを反映してね

これで実行してみると・・・

二重ループによって日付が出力される様子

はい、変数 i と変数 j それぞれの初期値であるCells(4, 2)のセルからそれぞれの最終値であるCells(14, 8)まで1ずつインクリメントする日付を出力しました。

当然なんの制御もしていないので「42日」というありえない日付まで出力しています。後続でこれを年月によって途中で切り上げるようにします。

では、話の流れ的にまずはこれに対して変数 j が正しい位置(1日目)から始まるようにしてみましょう。

前段で

変数 j に変数startColumnの値を設定してやればよいのでは?

と言っているので下記のソースコードを書きましょう

                j = startColumn

問題はどこに書くか、です。例えばなんの制御も設けずに下図のように書いた場合・・・

おためし実装

下記のような動きをします。

変数 j には毎週「8」の値が代入され、列ループは最終値が8だから毎週1周だけでおわる

はい、全行についてH列だけの出力になってしまいました。これは何が起きてるかと言いますと、下記のように検証用Debug.Printを追加して実行してみます。

        '列ループ
        For j = 2 To 8
            Debug.Print "変数 j の値(startColumn代入前):" & j
            j = startColumn
            Debug.Print "変数 j の値(startColumn代入後):" & j
            Cells(i, j).Value = day & "日"
            Application.Wait [Now() + "0:00:00.001"]
            day = day + 1
        Next
これは確認用のソースコードです。あなたも一緒にやってみて。あとで消すよ

実行すると・・・

2, 8, 2, 8, 2, 8, 2, 8…と変数 j の値が出力される

変数 j の値について、ループが2から始まったと思いきや強制的に8(土曜日にあたる値)を代入しています。その結果、本来なら7周するループが各行について1周だけしか機能しない、という働きになっています。

しかし、下図の完成形を見ていただいたらわかるように週の途中から出力が始まるのは1日目がある行の時だけなんですよね。他は必ず日曜日(B列)から始まります。

完成形の状態

と、いうことはここに縛りを追加する必要があります。縛りといえば条件分岐、条件分岐といえば if 文です。

でね、この変数 j に変数startColumnの値を代入するのは1日目を出力するときだけ(=行方向のループが1回目のときだけ)、と言いました。すると自然と下記のようなソースになります(まだ書かなくていい。見てて)。

まだ書かなくていいぞよ(さっきのDebug.Printはもう消してOK)

変数 i が行方向のループになるので、この値が4のとき、つまり1周目の時だけ変数 j に開始列位置を代入する処理が実行されるように書きました。その結果・・・

お、いいですね~。さっきよりよくなった

はい、うまくいきましたね。と思うでしょう?実は甘いんです。

今検証しているのが2024年6月だからたまたま変数startColumnは8をもっています。ここに罠がある・・・!

今回、変数 j をカウンタ変数とするループは終了値を8としています。そこに変数startColumnがもつ8を代入しました。変数 j の値が8の場合、次の周回に入ろうとするとき「もう終了値に達してるからループを抜けるよ」という動きをします。

その結果、外側のループのカウンタ変数 i をインクリメントし、次の処理に進むことができています。では変数startColumnの値が8ではなかったらどうなるのか。

うおおおおお!!!!なんじゃこりゃ!!

いかがでしょう。無限ループが起こっています。バグです。

これ、1行目の時に毎回変数 j に5を代入してるから永遠に変数 j の値がインクリメントされないんですよね。変数 j の値は列ループの2周目に入る時、内部で6になります。

ですが、その直後にまた5を代入するので、5→6→5→6→5→6・・・を延々繰り返します。その結果出力セルが移動しないという事態が起こっています。

だから先ほど案内した

            If i = 4 Then
                j = startColumn
            End If

この条件式では不十分なんです。もう一声ほしい・・・!

それでは何がほしいでしょうか。今1日目を出力しようとしている状態、というのは変数 i ,変数 j のどちらもが開始値であるときだけなんです。

先ほどやった

この状態で「1日」を出力しているのは変数 i ,変数 j が開始値であることがわかりますね。変数 i の開始値は4、変数 j の開始値は2です。「1日」は4行目の2列目であるB4セルに出力されています。

くどい説明になりましたが、この時の出力位置を上書きしてやろう、というのが今やろうとしていることです。「1日」を出す場所を強制的に書き換えてやろうと。

と、いうことは変数 j の状態も条件式に加えてあげましょう。下記のようになります。下記を下図のようにお願いします。

            If i = 4 And j = 2 Then
                '最初の周回は1日目の曜日列を設定する
                j = startColumn
            End If
赤枠のソースコードを反映してね

このロジックで2024年5月の場合と2024年6月の場合、どちらも確認します。

2024年5月、2024年6月の2つのパターンで実行する様子

正常に動いていますね。これで項タイトルである「1日」の出力列の設定は完了です。

次、いきたいのですが、ひとつ挟ませてください。


実行ボタンを用意しよう

Gifジフ画像を見ながら感じていた方もいるかもしれませんが毎回開発リボンからマクロを選択するのが煩わしいですね汗。ボタンを用意します。

操作忘れちゃった、という方は下図を参考にしてください。

ボタンを設定する様子

ちょっとフォントやそのサイズを調整しまして、↓こんな感じで行きます。

では、実装の話に戻しまして、

  • 「1日」の出力列の設定

  • 日付出力可否の判定(主に29,30,31日)

  • 日付に対する曜日情報の付与

  • 曜日に応じたセルの装飾(文字色、背景色)

  • 行幅、列幅の調整

これらのうち2つ目の要素に入ります。


日付出力可否の判定(主に29,30,31日)

はい、この項タイトルを読んでおやおや?と思ったあなた、鋭い。

そうです。少し前に今回の実装で考えたい要素を羅列したとき

今回初めてやるもの、これまでやってきたことをそのまま流用できるもの、応用して使うもの、様々です。

と、言いました。流用します!

次のソースを下図のように追記してください。

            If isDayWritable(year, month, day) Then
                Cells(i, j).Value = day & "日"
                Application.Wait [Now() + "0:00:00.001"]
                day = day + 1
            End If
赤枠のところを追加してね

これで実行しますと・・・

2024年6月、2024年5月どちらも正常動作している

余分な日付の出力が抑制されましたね。余分な日付とは32日以降のことです。

この項はこれでOKです。では次は3つ目いきましょう。

  • 「1日」の出力列の設定

  • 日付出力可否の判定(主に29,30,31日)

  • 日付に対する曜日情報の付与

  • 曜日に応じたセルの装飾(文字色、背景色)

  • 行幅、列幅の調整


日付に対する曜日情報の付与

完成形では

完成形のカレンダー

各日付に対して曜日情報が付与されています。これを実装しましょう。

今、日付を出力するソースがこちらです。

                Cells(i, j).Value = day & "日"

ここに処理を追加します。前回#15でやった関数calendarInitの中に

dayOfWeek = Weekday(year & "/" & month & "/" & 1)

こんな処理がありますね。変数dayOfWeekには曜日を示す値が入っている、という話でした。どの値がどの曜日かは以下のリファレンスを参照ください。前回も案内したものです。

さて、Microsoftさんが気を利かせてくれています。プログラムを提供する側としては曜日を表す数値を返す関数があれば、当然その数値が示す曜日を返す関数にも需要があるだろうと、思うようです(おそらくね)。

ちゃんと用意されています。その名もWeekdayNameウィークデイネーム関数。

上記リファレンスの構文の欄を下図に拝借しています。

一部キャプチャ

関数使用時に与える引数の説明です。最初のweekdayは必須、そして残り2つは省略可能とありますので、今回は第一引数のみ指定します。曜日を示す数値を指定したらいいんですね。

では、先ほど示した日付を出力する処理に少し変更を加えます。

  1. 曜日情報を数値で取得する

  2. その数値を曜日の文字列に変換する

の2段構えです。

以下のようにしてみましょう。下記を下図のようにお願いします。

                '値設定
                dayOfWeek = Weekday(year & "/" & month & "/" & day)
                Cells(i, j).Value = day & "日(" & WeekdayName(dayOfWeek) & ")"
赤枠が反映して欲しいソース

これで実行すると・・・

あ~いいですねっ

はい、「1日(土曜日)」と出ました。う~ん、惜しい!もう一声!

文字列中の「曜日」の部分は省略したいので、もう少し加工します。

Leftレフト関数というものがありまして

これは文字列に対して左から指定数分文字を切り出す、という働きをします。

なので、「土曜日」に対して左から1文字だけ切り出せば「土」を抽出することができますね。

引数情報の説明を見てみましょう。

Left関数の引数の説明

どちらも必須で第一引数に処理したい文字列を、第二引数に切り出したい文字数を指定します。では、使ってみましょう。

                Cells(i, j).Value = day & "日(" & Left(WeekdayName(dayOfWeek), 1) & ")"
赤枠のところを反映してね

これで実行してみます。

お、いいですね~。

はい、うまくいきましたね。期待値が得られました。

今のソース、念のためもう少し詳細に解説させてください。

Cells(i, j).Value = day & "日(" & Left(WeekdayName(dayOfWeek), 1) & ")"

ここの曜日を取得する処理、関数が入れ子になってて初学者にはちょっと複雑かもしれません。土曜日を例にします。

まず、土曜日を示す数値は7なので処理を分解してみてみると、最初に

Cells(i, j).Value = day & "日(" & Left(WeekdayName(7), 1) & ")"

このような状態になっています。変数dayOfWeekを「7」にしました。次に、WeekdayName関数が働くので

Cells(i, j).Value = day & "日(" & Left("土曜日", 1) & ")"

このような形になります。「WeekdayName(7)」が「"土曜日"」に置き換えられます。そしてLeft関数で左から1文字を切り出すので

Cells(i, j).Value = day & "日(" & "土" & ")"

ということになります。「Left("土曜日", 1)」を「"土"」に置き換えました。ここまでくると、関数により動的に値を取得するために「&」を使って文字を連結させていたので、それさえも省略すると・・・

Cells(i, j).Value = day & "日(土)"

こうなります。こうなると#5でやったカレンダーLv.1と同じくらいの単純さですね。変数dayとその後ろに文字列を繋げており、それをセルに代入しています。

はい、この項はこれで終わりにします。

次は4つ目です。

  • 「1日」の出力列の設定

  • 日付出力可否の判定(主に29,30,31日)

  • 日付に対する曜日情報の付与

  • 曜日に応じたセルの装飾(文字色、背景色)

  • 行幅、列幅の調整


曜日に応じたセルの装飾(文字色、背景色)

お次はセルの背景と文字に色をつける処理を書きます。と、タイトルには含めていませんが、罫線の描画もやっちゃいます。

各セルの背景色と文字色、罫線に注目

さて、この項では2つの関数を作成します。

  • セル塗色用の色配列を返す関数

  • セルに装飾をほどこす関数

では、一つ目の色配列からやりましょう。実はこれ、あみだくじでもやっているんですよね。もう完成形のソースコードをご案内します。これをcalendarInit関数の下に追記してください。

'セル塗色用の色配列を取得する
Function getDayColors()
    Dim colors(2 To 8) As Long
    colors(2) = 15921906
    colors(3) = 14998742
    colors(4) = 14083324
    colors(5) = 15592941
    colors(6) = 13431551
    colors(7) = 14348258
    colors(8) = 15719154
    getDayColors = colors
End Function
この関数を追記してね

1点だけ伝えることがあります。宣言について、あみだくじの時は

    Dim colors(10) As Long

こんな風に書いて、いざ使うときには

repaintColor = colors(tryLotIndex - 1)

このように「- 1」とすることでインデックスとのずれを調整しました。配列は0番目から始まるから、ということだったのですが、これ、もう少しスマートに書けます。

    Dim colors(2 To 8) As Long

↑このように宣言することで、この配列は2番目から始まり8番目で終わります!という風にできるんです。今回、週ごとの日付を出力するときに使う変数 j は2から8を使うので、これで色配列の添え字とリンクします。

色配列の準備に関してはこれでOK。各色を自分流にカスタマイズしたい!という方はあみだくじでやった

Sub a()
    Debug.Print Selection.Interior.Color
End Sub

この1次的に使用する関数で色値を取って設定してみてください。

では、もうひとつのセルを装飾する関数を作ります。

'日付を出力するセルを装飾する
Function cellDecoration(targetRange As Range)

End Function
赤枠のところを反映してね

まずは側です。上記ソースをcalendarInit関数とgetDayColors関数の間に挿入してください。

この処理は装飾対象のセルが必要なので、引数にはRange型の変数を受け取るようにしています。セルそのものを受け取る、というわけです。

さて、まずは色配列を用意する必要があります。さらに色の設定までやってしまいましょう。

    '塗り替える色の取得
    Dim colors() As Long: colors = getDayColors()
    'セル背景色設定
    targetRange.Interior.Color = colors(targetRange.Column)
赤枠が反映して欲しいソース

ここまでかけたら動かしてみたいので、呼び出す処理も書きます。下記のソースをループ処理中に下図のように追記してください。

                'セルの装飾
                Call cellDecoration(Cells(i, j))
赤枠のソースを追記してね

これで実行してみます。

お、いいですね~

いや~一気にそれっぽくなりましたね。これだけ見てたら「別に罫線は要らないのでは?」という気がしてしまいます。

と、あみだくじでもうやってるから慣れたものでしょうけど、変数 j の値を配列の添え字として使うことでスマートに各曜日に異なる色を設定できる点、最高ですよね。

こういう手を使わなかったらIfとelseIfを使って、日曜ならこの色、月曜ならこの色、火曜ならこの色・・とクドクド書くことになるわけですから。


では、罫線の描画をやります。次のソースコードをcellDecoration関数内に追記してください。

    '罫線設定
    Range(targetRange, targetRange.Offset(1, 0)).BorderAround True
    targetRange.Borders(xlEdgeBottom).LineStyle = xlDot
赤枠のソースコードを追記してね

まず、動作を見てみます。

罫線が描画されている点にご注目

はい、いいですね。罫線の描画についても公式リファレンスがあるのですが、私は有志の方のサイトを覗くことの方が多いです。その方が必要な情報がわかりやすくまとめられていたりするのでね。

今回のソースを簡単に解説しますと・・・

    '罫線設定
    Range(targetRange, targetRange.Offset(1, 0)).BorderAround True
    targetRange.Borders(xlEdgeBottom).LineStyle = xlDot

まず1行目の処理で

罫線を設定する処理の1行目がやっていることの説明

上図の通り、2マスを囲む罫線を引いています。.|BorderAround《ボーダーアラウンド》言うてますからね、文字通りセルの周りの線です。

そして2行目で・・・

罫線を設定する処理の2行目がやっていることの説明

2マスの間を仕切るところに点線を引いています。.LineStyleに「xlDot」ですから、線のスタイルをドット状にする、ということをしています。

では最後に、土曜日と日曜日の青字、赤字をやりましょう。次のソースコードを罫線処理に続いて追記ください。

    '土日文字塗色
    If targetRange.Column = 2 Then
        targetRange.Font.Color = RGB(255, 0, 0)
    ElseIf targetRange.Column = 8 Then
        targetRange.Font.Color = RGB(0, 100, 255)
    End If
赤枠のソースコードを追記してね

解説の前に動作を見てみます。

土曜日、日曜日の文字色が青、赤になっている点にご注目

うまくいきましたね。ここのソースコードは特に目新しい要素はないです。文字色変更は世界のなべあつで扱いました。2列目だったら赤色を、8列目だったら青色を文字色とする処理をしています。

targetRange.Column

この処理で、Rangeが所属するセルが何列目なのか?を取得できます。そしてそれを条件式で数値と比較しているわけですね。

セル装飾の関数はこれで完了です。最後、いきましょう。

  • 「1日」の出力列の設定

  • 日付出力可否の判定(主に29,30,31日)

  • 日付に対する曜日情報の付与

  • 曜日に応じたセルの装飾(文字色、背景色)

  • 行幅、列幅の調整


行幅、列幅の調整

今の状態でもほぼ完成のような感じはしています。ただ、印刷して実際に使えるくらいのクオリティにはもう一声ほしい。

というわけで、最後に行幅・列幅を調整する処理を書きます。これで実際になにか予定を入力したり書き込んだりすることができるようになりますね。

では、今回もいきなりソースをご案内しますが、まだ完成形ではありません。ひとまず、見ててください。

            Rows(i).RowHeight = 20
            Rows(i + 1).RowHeight = 34
            Columns(j).ColumnWidth = 10

このソースコードで、行幅と列幅を変更できるので、これをループ内に設置してみます。

動作を見てみると・・・

行の高さ、列幅の調整処理も動いている様子

問題なくできています!これでOKではある。

が、もう一押ししましょう。

幅調整処理が過剰に実行されないように気付きを促す図。あれ、6と7逆・・・

今のソースだと行の高さの変更を42回、列幅の変更も42回しています。でも実際は行の高さの調整は6回、列の横幅の調整は7回でいいんです。

あとは必ずこれが実施されるタイミングをIf文の条件式に落とし込むだけです。下記のソースを下図のように挿入ください。

            If j = 2 Then
                '行の高さ調整
                Rows(i).RowHeight = 20
                Rows(i + 1).RowHeight = 34
            End If
            If i = 6 Then
                '列の横幅調整
                Columns(j).ColumnWidth = 10
            End If
赤枠のソースコードを追記してね

これで実行すると・・・

効率的に行の高さ・列幅を調整している様子

これ、鋭い人は分かると思うんですが、先ほどとの違いわかります?わずかにパフォーマンスが改善してるんです。描画速度がさっきより速い。

というのも列幅調整は変数 i の2周目、行の高さ調整は変数 j の1周目で行っているので・・・

赤枠のセルに描画するタイミングで行・列の幅を調整している

この赤枠で囲んだセルを処理するときだけ幅調整をしていて、それ以外の時はこの処理はスルーしています。これで無駄な処理を省きました。

そして処理速度が少し向上したわけです。見たところ、値の出力やセルの装飾に比べて行の高さや列幅の変更はちょっと時間がかかるようですね。

これですべてクリアです。最後におまけです。実装忘れではない!


おまけ

今までずっとやってきたのに今回やってない処理がひとつあるの、お気づきですか?

そう、処理の進行に合わせて選択セル位置が追随してくる処理を書いていませんでした。

次の1行を下図の位置に挿入ください。

                Cells(i, j).Select
赤枠のソースコードを追記してね

はい、では仕上げにもう一度だけ動作を見ておきましょう!

完成動作っ

(*・ω・)(*-ω-)(*・ω・)(*-ω-)ウンウン♪素晴らしい。

カレンダーLv.6はこれで終わりです!


今回の補足

関数の並び順

今回ね、cellDecoration関数を作るときに

上記ソースをcalendarInit関数とgetDayColors関数の間に挿入してください。

って言ったじゃないですか。出てきた順で言うとgetDayColors関数のあとでもよさそうなのに。

これはちょっとしたこだわりというか指針がありまして、ソースコードの働きや重要度、参照する回数を考慮してあまり重要でないものや単純なことしかしていないものは後ろにもってこようという意図があります。

それによって上記のような順番をお願いしました。

大したことではないのですが、ちょっとしたことでもこだわりや実装者の考えが見えるとより美しい、均整のとれたソースコードとなります

美は細部に宿るって言いますからね。


おわりに

おわりです!

カレンダーLv.6は二重ループ処理の活用をメインに据えていました。いかがでしたでしょうか。二重ループはいろんな機能で使えるので、考え方をここで習得しておくと今後も役に立つこと請け合いです。

エクセルを使った学習は処理内容を視覚的に確認できるのがいいですね。

それでは、今回も脳ミソをゴリゴリ使ったと思います。ゆっくりお休みになってください。残りあと2回です!

今回もありがとうございました。よく寝てよく休んで。


この記事が気に入ったらサポートをしてみませんか?