和風スパゲティのレシピ

日本語でコーディングするExcelVBA

プロシージャ[Sub/Function]の使い方入門

マクロを作るとき、真っ先に書くことになるSubやFunctionのことを、
正確には「Subプロシージャ」「Functionプロシージャ」と呼びます。


みなさんはこの「プロシージャ」という機能を、
どのくらい使っていますか?


私の知る範囲の話になってしまいますが、

  • マクロは基本1つのSubプロシージャに書いている
  • Functionプロシージャの存在は知っているけど一度も使ったことがない
  • 書き方はわかるけど使いどころがわからない

ということにお悩みの方が、とても多い印象を受けます。


特に「書き方の前に使いどころがわからない」というのがこの機能の特徴で、
これが学習の壁になっている方が多いのではないでしょうか?


そんな方々へ向けて、プロシージャ分割の基本を解説いたします。


長大な記事になってしまいましたが、「書き方」「使い方」「使いどころ」をセットで学べる構成にしたつもりですので、ぜひ最後までお付き合いください。

本記事はしゃあさん主催のVBA勉強会「VBAの悩みはVBAerに聞け」にて発表した、「Sub/Functionの使い方入門」の内容をテキストに起こした記事となります。

ご参加くださった皆様ありがとうございました!

実際の会の内容はしゃあさんが議事録にまとめてくださっておりますので、
こちらもぜひご覧ください。https://note.com/char0059/n/n9ba1e317669f

※ 本記事単独で読める内容にしておりますのでご安心ください。


Sub・Functionプロシージャとは

さて冒頭に書きました、

  • Functionプロシージャの存在は知っているけど一度も使ったことがない
  • マクロは基本1つのSubプロシージャに書いている
  • 書き方はわかるけど使いどころがわからない

このお悩みですが、何故この悩みを抱えてしまうのかをまず考えましょう。


その原因はズバリ、
プロシージャがないと実装できない処理はないから
です。


例えばセルに値を入れるには「Rangeオブジェクト」、
オートフィルターを使うには「AutoFilterオブジェクト」のように、
何かをするために必ず必要になる機能というものがあります。


しかし、プロシージャにはこういった「これをやるにはプロシージャの作成が必要!」という処理がありません。

※もちろん最初のSubプロシージャ1つは必要です。


究極どんな処理でも、プロシージャを使わずに実装することが可能です。
(現実的かはさておき)


逆に言えばプロシージャを使わざるを得ない状況が訪れないので、
そのせいで「使いどころがわからない」という状況に陥るわけです。


ではプロシージャとは何のためにある機能なのか?


答えは「コードを書きやすく&読みやすくするため」にあります。

  • プロシージャがないと書けない処理はない
  • プロシージャがあると処理が書きやすくなる

ということなんですね。


ではプロシージャによってどんな風に処理が書きやすくなるのかというと、

Sub・Functionプロシージャを使うことで、
処理のまとまりに見出しを付けて1行に要約する
ことができるようになります。


これがどういう意味なのかを、いきなりコードを見てもイメージしづらいと思います。

プログラムからちょっと離れて、
↓のレシピを適当に眺めてみて下さい。


簡単!しらすとすだちのオイルパスタ

1.使う調理器具


・まな板・包丁
・パスタ鍋
・フライパン

2.使う食材と調味料(1人分)


・スパゲティ(乾麺・細め) 100g
・釜揚げしらす … 40g
・すだち … 2個
・にんにく … 1片
・青ネギ … 2本
・鷹の爪輪切り … 少々
・オリーブオイル … 大さじ3

3.下ごしらえ


・すだち … 輪切りスライスを2枚、残りは半分に
・にんにく … みじん切り
・青ネギ … 小口切り

4.麺をゆでる


1.パスタ鍋にたっぷりのお湯を沸かす
2.塩を入れる(2%)
3.麵を入れる
4.6分でゆであがり

5.ソースを作る


1.火にかけていないフライパンにオイル・にんにく・鷹の爪を入れる
2.火をつける(弱火)
3.にんにくと鷹の爪の香りをオイルにじっくりうつす
4.ゆであがり1分前になったらおたま1杯分をフライパンに入れる(中火)

6.麺とソースを和える


1.ゆであがったパスタをお湯を切ってフライパンに入れる
2.軽く混ぜてからしらす・青ネギ(2/3)を入れる
3.味見して塩を入れる
4.素早く全体を和える

7.盛り付け


1.お皿に盛りすだちを絞る
2.残りの青ネギをふりかける
3.スライスしたすだちを飾ってできあがり




唐突過ぎて面食らったかもしれませんが、
このレシピに使った「詳しく見る」「詳細を閉じる」ボタンこそがプロシージャです。

確かに「処理のまとまりに見出しを付けて1行に要約」していますよね?


WEBページにはよくあるこの機能、
開閉する様子から「アコーディオンボックス」と呼ぶようですが、

  • すべて閉じれば全体の流れがつかめる
  • 細かく見たい部分だけ開いて見れる

というメリットがあるのがわかります。


これと同じことをコードでやるために存在するのがプロシージャなんですね。


この機能をプログラミングに当てはめて考えると、

  • メインコードでは全体の流れがつかめる
  • 各プロシージャでは処理の詳細がわかる
  • 読みたい処理だけ細かく見て、その他の処理は読み飛ばすことができる

という恩恵を得ることができます。


ただし、プログラミング言語(VBA)には、
この「開閉ボタン(アコーディオンボックス)」の機能そのものはありません。

なので、Subプロシージャを使って、
実際は↓のように書くことになります。

Sub しらすとすだちのオイルパスタを作る()
    Call 使う調理器具
    Call 使う食材と調味料(1)
    Call 下ごしらえ
    Call 麺をゆでる
    Call ソースを作る
    Call 麺とソースを和える
    Call 盛り付け
End Sub

Sub 使う調理器具()
    調理器具.Add "まな板"
    調理器具.Add "包丁"
    調理器具.Add "パスタ鍋"
    調理器具.Add "フライパン"
End Sub

Sub 使う食材と調味料(n人分 As Long)
    食材リスト.Add "スパゲティ乾麺細め", 100 * n人分
    食材リスト.Add "釜揚げしらす", 60 * n人分
    食材リスト.Add "すだち", 2 * n人分
    食材リスト.Add "にんにく", 1 * n人分
    食材リスト.Add "青ネギ", 2 * n人分
    食材リスト.Add "鷹の爪輪切り"
    食材リスト.Add "オリーブオイル", "大さじ", 3 * n人分
End Sub

Sub 下ごしらえ()
    食材リスト("すだち").Slice "輪切り"
    食材リスト("にんにく").Cut "すりおろし"
    食材リスト("青ネギ").Cut "小口切り"
End Sub

Sub 麺をゆでる()
'割愛
End Sub

Sub ソースを作る()
'割愛
End Sub

Sub 麺とソースを和える()
'割愛
End Sub

Sub 盛り付け()
'割愛
End Sub


このように、アコーディオンの中身は別のSubプロシージャとして下に書いて、
それをメインコード内で「Call」によって呼び出すことで、
「処理のまとまりに見出しを付けて1行に要約する」を実現します。

これがプロシージャの役割です。


これからプロシージャ分割のテクニックを一通り紹介していきますが、

どこまでいってもプロシージャの目的は、この
処理のまとまりに見出しを付けて1行に要約する
で最後まで変わりません。


機能面(手段)を学ぶとどうしても目的がぼやけてしまいますので、
このことを忘れないように読み進めてください。

プロシージャを扱う上で必須のショートカット3選

さて実際のコーディングに入る前に、
プロシージャを使うために必要なショートカットキーを覚えていただきます。


まずプロシージャを「開閉ボタン(アコーディオン)」として使うには、
「Call プロシージャ名」から実際のプロシージャへジャンプする機能が必要です。

これがないと、Callを見るたびにマウスのホイールを回す羽目になりますからね。


この「プロシージャの中身までジャンプ」するのが「Shift + F2」です。


また、中を見終わったら戻ってくる(アコーディオンを閉じる)ことも必要で、
そのショートカット「元の位置に戻る」が「Ctrl + Shift + F2


この二つのショートカットが、アコーディオンを開いたり閉じたりするボタンの代わりになりますので、まずはこの2つをしっかり覚えておきましょう。



また、プロシージャ名は「見出し」だけあって、長くなることが多いです。

いちいち正確に同じ名前をタイピングするのは大変ですので、
コードを途中まで打ったら残りを補完してくれるCtrl + Space
も絶対に覚えておくべきショートカットです。



なお、この3つはプロシージャ以外にも使用でき、
F2コンビは「変数名からDimまでジャンプ」「元の位置に戻る」にも使えます。

Ctrl+Spaceは「Works」⇒「Worksheet」などの基本構文や、
自分で作った変数の補完にも使えますね。


特にCtrl+Spaceはプロシージャ分割関係なく、
VBAにおけるもっとも重要なショートカットですので、
必ず習得しておきましょう。


プロシージャ分割の進め方

さて実際のコーディングに入りたいと思いますが、
いきなりプロシージャから作り始めるのは、
実は相当な訓練が必要です。


プロシージャ分割の一番簡単な方法は
完成したコードを切り取って分割する
ことなので、それを繰り返し訓練していきましょう。


ということで、プロシージャ分割の方法を説明するには、
元のコードが必要になります。

サンプルコードを用意しましたのでご覧ください。


このようなデータシート(年度ごとに別シート)があったとき、

元データシート


この操作用シートで条件を指定して、
必要なデータを取ってくるマクロを作ります。

抽出シート


例では2020年度にりんごを500円以上売り上げた取引を抽出しています。


このソースコードを例に、プロシージャ分割をしていきましょう。


以下のソースコードを見ていただくか、
勉強会開催用にしゃあさんよりアップロードいただいた、
以下のサンプルファイルをダウンロードしてみてください。
Excelファイルをダウンロード



繰り返しになりますが、プロシージャ分割とは「見出しを付けること」です。

この訓練に中身のコードはそんなに重要ではありませんので、
まずは流れがつかめるよう、コードは流し見程度に読み進めてください。

分割前のソースコード

Sub くだもの売上データをシート指定条件で抽出する_Sub分割なし()

    Dim ws抽出シート As Worksheet
    Set ws抽出シート = Worksheets("抽出シート")

    If ws抽出シート.Range("C2") = "" Then
        MsgBox "対象年度を入力して下さい。"
    Else
        Dim ws As Worksheet
        Dim Is対象年度のシートが存在する As Boolean
        For Each ws In ThisWorkbook.Worksheets
            If ws.Name = "くだもの売上データ(" & ws抽出シート.Range("C2") & "年度)" Then
                Is対象年度のシートが存在する = True
            End If
        Next

        If Is対象年度のシートが存在する = False Then
            MsgBox "指定年度のデータがありません。"
        ElseIf ws抽出シート.Range("C3") = "" Then
            MsgBox "商品名を入力してください。"
        ElseIf Worksheets("くだもの売上データ(" & ws抽出シート.Range("C2") & "年度)").Columns(6) _
        .Find(ws抽出シート.Range("C3"), lookat:=xlWhole) Is Nothing Then
            MsgBox "入力された商品名はデータに登録されていません。"
        ElseIf ws抽出シート.Range("C4") = "" Then
            MsgBox "条件売上額を入力してください。"
        ElseIf IsNumeric(ws抽出シート.Range("C4")) = False Then
            MsgBox "売上額は数値で入力してください。"
        Else

            Application.ScreenUpdating = False
            Application.EnableEvents = False
            Application.Calculation = xlCalculationManual
            Application.Cursor = xlWait

            ws抽出シート.Range(ws抽出シート.Rows(7), ws抽出シート.Rows(ws抽出シート.Rows.Count)).Delete

            Dim wsデータ As Worksheet
            Set wsデータ = Worksheets("くだもの売上データ(" & ws抽出シート.Range("C2") & "年度)")
            Dim LastRデータ As Long
            LastRデータ = wsデータ.Cells(Rows.Count, 1).End(xlUp).Row

            Dim R抽出シート As Long: R抽出シート = 7
            Dim Rデータ As Long
            For Rデータ = 2 To LastRデータ

                If wsデータ.Cells(Rデータ, 6) = ws抽出シート.Range("C3") _
                And wsデータ.Cells(Rデータ, 9) >= ws抽出シート.Range("C4") Then

                    ws抽出シート.Cells(R抽出シート, 3) = wsデータ.Cells(Rデータ, 4)
                    ws抽出シート.Cells(R抽出シート, 4) = wsデータ.Cells(Rデータ, 6)
                    ws抽出シート.Cells(R抽出シート, 5) = wsデータ.Cells(Rデータ, 9)

                    If wsデータ.Cells(Rデータ, 1) >= 4 Then
                        ws抽出シート.Cells(R抽出シート, 2) _
                            = DateSerial(ws抽出シート.Range("C2"), wsデータ.Cells(Rデータ, 1), wsデータ.Cells(Rデータ, 2))
                    Else
                        ws抽出シート.Cells(R抽出シート, 2) _
                            = DateSerial(ws抽出シート.Range("C2") + 1, wsデータ.Cells(Rデータ, 1), wsデータ.Cells(Rデータ, 2))
                    End If

                    R抽出シート = R抽出シート + 1

                End If

            Next

            With ws抽出シート.Range(ws抽出シート.Cells(7, 2), ws抽出シート.Cells(R抽出シート - 1, 5))
                .Borders.LineStyle = True
                .Borders.Weight = xlThin
                .Borders(xlInsideVertical).Weight = xlHairline
                .Borders(xlInsideHorizontal).Weight = xlHairline
            End With

            Application.ScreenUpdating = True
            Application.EnableEvents = True
            Application.Calculation = xlCalculationAutomatic
            Application.Cursor = xlDefault

            MsgBox "データの抽出を完了しました。"

        End If
    End If
End Sub

実際のプロシージャ分割(基本編)

ではこれをプロシージャに分けていきましょう。


何度も言っていますが、どこまでいってもプロシージャの目的は
処理のまとまりに見出しを付けて1行に要約する
で最後まで変わりませんので、これを忘れないように読み進めてください。

マクロの高速化四天王 ~Subプロシージャ~

メインコードのスタートと同時に設定することが多い、

Application.ScreenUpdating = False
Application.EnableEvents = False
Application.Calculation = xlCalculationManual
Application.Cursor = xlWait

まずはこの部分を切り取り、新しいプロシージャにします。


単独で実行できる部分ですので、
普通に新しいマクロを作る感じで分割できます。


プロシージャ名=見出しなので、

Sub Excelの自動更新を停止する()
    Application.ScreenUpdating = False
    Application.EnableEvents = False
    Application.Calculation = xlCalculationManual
    Application.Cursor = xlWait
End Sub

こんな感じの名前を付けて、Subプロシージャを作りましょう。


このSubプロシージャを作ったことで、元のソースコードは、

Call Excelの自動更新を停止する

この1行で済むようになります。


もうひとつ、マクロの最後に高速化設定を戻す

    Application.ScreenUpdating = False
    Application.EnableEvents = False
    Application.Calculation = xlCalculationManual
    Application.Cursor = xlWait

こいつらも、

Sub Excelの自動更新を再開する()
    Application.ScreenUpdating = True
    Application.EnableEvents = True
    Application.Calculation = xlCalculationAutomatic
    Application.Cursor = xlDefault
End Sub

こうしてSubプロシージャにすれば、

メインコードは、

Call Excelの自動更新を再開する

この1行でOK。


一連の作業の流れは、

  1. 出来上がったコードを切り取って外へ出す
  2. SubとEnd Subで挟む
  3. 名前=見出しを付ける
  4. 元のメインコードでCall 名前

とするだけです。

完成したコードをプロシージャ分割するのは、
意外と簡単なのがわかりますよね。


ここで一つポイントとなるのが「見出し」のつけ方で、
この「見出し」は既にメインコードに書いてあることがあります。


そう

' Excelの自動更新を停止
Application.ScreenUpdating = False
Application.EnableEvents = False
Application.Calculation = xlCalculationManual
Application.Cursor = xlWait

この「コメント」です。


こんな風にコードのブロックに見出しコメントを付けている方は、

見出しコメントをCallに書き換えること ⇒ プロシージャ分割

と簡単に考えることができますので、この着眼点は覚えておきましょう。

行の削除(シート初期化)部分 ~引数の使い方~

抽出シートを更新する前に、前回の検索歴を削除している

ws抽出シート.Range(ws抽出シート.Rows(7), ws抽出シート.Rows(ws抽出シート.Rows.Count)).Delete

この部分もプロシージャにするといい場所です。

たった1行ですが、ぱっと見は何やってるかわからないコードですからね。


プロシージャ分割は長いコードを分けるためにやっているわけではなく、
見出しを付けるためにやっていますので、
たとえ1行でも見出しを付けたければプロシージャを作ってOKです。


先ほどの手順で新しいSubプロシージャに分割してみると、

' メインコード内
Call 指定行より下の行をすべて削除する
' 作成プロシージャ
Sub 指定行より下の行をすべて削除する()
    ws抽出シート.Range(ws抽出シート.Rows(7), ws抽出シート.Rows(ws抽出シート.Rows.Count)).Delete
End Sub

こんな風に書けます。


なんですが、これを実行すると、
変数(ws抽出シート)が定義されていません
というエラーになります。


プロシージャの重要な仕様として「変数はそれぞれ独立」なので、
メインコード内のws抽出シートはこのままでは使えないんですね。

これは困った仕様ではなく、とても大事な仕様です。
なぜならこの仕様じゃないと「i」すら使いたい放題できないからです。
全プロシージャの「i」が連動するなんて考えただけで恐ろしい・・・


これを解決するのが「引数」で、

' メインコード内
Call 指定行より下の行をすべて削除する(ws抽出シート)
' プロシージャ
Sub 指定行より下の行をすべて削除する(ws抽出シート As Worksheet)
    ws抽出シート.Range(ws抽出シート.Rows(7), ws抽出シート.Rows(ws抽出シート.Rows.Count)).Delete
End Sub

このように、
Dimの代わりにカッコ内にそっくり変数宣言
を書いて、
それをメインコードのカッコ内にも書いてあげる
ことで、分割したプロシージャに変数を渡すことができます。


この引数というのが最初は難しく、
なんとなくわかった気になったかもしれませんし、
いまいちわからない方もいるかもしれません。

わかったつもりでも実際に書いてみようとすると、
とたんにわからなくなったりします。


なので、ここはあまり考え過ぎない(理解してから先に進もうとしない)のがいいと思います。


プロシージャ分割は見出しを付けて「全体の流れ」をつかむのが目的です。

全体の流れさえつかんでおけば、なんとなくわかってくると思いますので、
慣れてきたら思い出して勉強するくらいの気持ちで読み進めてください。



さてこれでプロシージャ分割は完了したのですが、
この「引数」はメインコードと分割したプロシージャで同名である必要はありません。


この「ある行より下の行をすべて削除」はいろいろなマクロで使いまわせますので、
「ws抽出シート」を「ws指定シート」に変えて汎用プロシージャにしてみましょう。

' メインコード内
Call 指定行より下の行をすべて削除する(ws抽出シート)
' プロシージャ
Sub 指定行より下の行をすべて削除する(ws指定シート As Worksheet)
    ws指定シート.Range(ws指定シート.Rows(7), ws指定シート.Rows(ws指定シート.Rows.Count)).Delete
End Sub

 
いいかんじですが、
ここでもう一つ引数にしたいものがありますね?


そう「7」です。


このままだと「7行目から先のすべての行を削除する」ですので、
これも引数にすることでこのプロシージャが真に汎用的な部品となります。

' メインコード内
Call 指定行より下の行をすべて削除する(ws指定シート, 7)
' プロシージャ
Sub 指定行より下の行をすべて削除する(ws指定シート As Worksheet, 削除開始行 As Long)
    ws指定シート.Range(ws指定シート.Rows(削除開始行), ws指定シート.Rows(ws指定シート.Rows.Count)).Delete
End Sub

 
これで完成ですね!


この

Call 指定行より下の行をすべて削除する(ws抽出シート, 7)

というコードを眺めていると、
なんとなく「引数」というものがつかめてくるのではないでしょうか?



さて、ここで一つポイントとして押さえておきたいのが、

ws指定シート.Range(ws指定シート.Rows(7), ws指定シート.Rows(ws指定シート.Rows.Count)).Delete

このコードから引数として抽出した「ws抽出シート」と「7」です。

この2つは、
コードをコピペしてきたときに書き換える部分
と考えることができます。


つまり、いつもみなさんがやっている
「コードをコピペして書き換える」作業は、

コードをプロシージャにして書き換え箇所を引数にする
作業にそっくり置き換えることができるということです。


プロシージャの設計をする際に、

引数 = コピペコードの書き換え箇所

という考え方は非常にわかりやすいポイントになりますので、
必ず押さえておきましょう。

最終行の取得 ~Functionプロシージャ

次はおなじみ「最終行の取得」をプロシージャにしてみましょう。

Dim LastRデータ As Long
LastRデータ = wsデータ.Cells(Rows.Count, 1).End(xlUp).Row

このコードって「読めるけど書けない」薔薇みたいなコードで、
空で書ける人が意外に少ないコードです。

プロシージャにしてしまえばもう暗記したり探しに行ったりしなくて済みますね。


さて、今までのコードは「メインコードから切り取って外に出す」
ことで分割を行っていましたが、

Dim LastRデータ As Long
LastRデータ = wsデータ.Cells(Rows.Count, 1).End(xlUp).Row

これを丸ごと切り取るわけにはいきませんよね?

メインコードでもこの変数と値は使いますから、
ここを全部持っていったらメインコードが動かなくなります。


ということで、これを切り取るには、

LastRデータ = ここから右側を切り取り

と、イコールの右側だけを切り取る必要があります。


こんな時に使うのが「Function プロシージャ」です。


書き方はもう覚えるしかないのですが、

' メインコード
LastRデータ = 最終行を取得する
' プロシージャ
Function 最終行を取得する As Long
    最終行を取得する = wsデータ.Cells(Rows.Count, 1).End(xlUp).Row
End Function

↑こんな風に

  • 切り取ったイコールの右側にFunctionの名前を書く
  • Functionの中身は「Function名 = 切り取ったコード」と書く
  • Function名の最後にそのFunctionの型をつける

と書くことでFunctionプロシージャを使うことができます。


あとは「行の削除」でやったときと同様、
シートや列番号など「書き換えたい場所を引数に」切り出して、

' メインコード
LastRデータ = 最終行を取得する(wsデータ, 1)
' プロシージャ
Function 最終行を取得する(ws指定シート As Worksheet, 指定列 As Long) As Long
    最終行を取得する = ws指定シート.Cells(ws指定シート.Rows.Count, 指定列).End(xlUp).Row
End Function

これで完成となります。


ちょっと手順が複雑で困惑するかもしれませんが、

LastRデータ = 最終行を取得する(wsデータ, 1)

ここを「データシートの1列目の最終行を取得」と読めば、
なんとなくやっていることがわかると思います。


この「なんとなくわかる」が大事。


Functionプロシージャはシステム的な仕様を暗記しなければいけない部分が多く、
最初はちょっと戸惑うかもしれません。


そんな時は目的に立ち返って、

  • プロシージャ分割は「見出しを付けて1行にする」ためにやっている
  • Functionはイコールの右側に見出しを付けている

だったことを思い出してください。

その他の分割テクニック

  1. マクロの高速化四天王 ~Subプロシージャ~
  2. 行の削除(シート初期化)部分 ~引数の使い方~
  3. 最終行の取得 ~Functionプロシージャ

上の3つでプロシージャの基本はカバーできました。

これで基本構文はマスターできますので、
まずは上の3つの分割を読み込んでみてください。


ここからは「どこを分割すると良いか」というわけどころの話になります。

使う機能・仕様は上の3つと変わりませんので、
詳細は割愛して分割のBefore/Afterだけ並べます。


プロシージャの使いどころとして、参考にしてみてください。

※ 例によって大事なのは「流れ」です。
 コードは重要でないので読み飛ばし推奨

シートの存在チェック

◇ Before

Dim ws As Worksheet
Dim Is対象年度のシートが存在する As Boolean
For Each ws In ThisWorkbook.Worksheets
    If ws.Name = "くだもの売上データ(" & ws抽出シート.Range("C2") & "年度)" Then
        Is対象年度のシートが存在する = True
    End If
Next

If Is対象年度のシートが存在する = False Then
    MsgBox "指定年度のデータがありません。"

◇ After

' メインコード
If Isシートが存在する(ThisWorkbook, SheetNameくだもの売上データ(Range("C2"))) = False Then
    MsgBox "指定年度のデータがありません。"
' プロシージャ
Function Isシートが存在する(wb指定ブック As Workbook, 判定シート名 As String) As Boolean

    Dim ws As Worksheet
    For Each ws In wb指定ブック.Worksheets
        If ws.Name = 判定シート名 Then
            Isシートが存在する = True
            Exit Function
        End If
    Next

    Isシートが存在する = False
End Function

◇ ポイント

Booleanを返すFunctionを「Is○○」にすると、

If Isシートが存在する Then

みたいに文章として読めるようになっておすすめ!

「イコールの右側」だけでなく、
「Ifの中身」もFunctionの使いどころ
です!

罫線(外を実線、中を最細線)

◇ Before

With ws抽出シート.Range(ws抽出シート.Cells(7, 2), ws抽出シート.Cells(R抽出シート - 1, 5))
    .Borders.LineStyle = True
    .Borders.Weight = xlThin
    .Borders(xlInsideVertical).Weight = xlHairline
    .Borders(xlInsideHorizontal).Weight = xlHairline
End With

◇ After

' メインコード
Call 指定エリアの外枠に実線、中枠に最細罫線を引く _
    (ws抽出シート.Range(ws抽出シート.Cells(7, 2), ws抽出シート.Cells(R抽出シート - 1, 5)))
' プロシージャ
Sub 指定エリアの外枠に実線、中枠に最細罫線を引く(罫線設置エリア As Range)

    With 罫線設置エリア
        .Borders.LineStyle = True
        .Borders.Weight = xlThin
        .Borders(xlInsideVertical).Weight = xlHairline
        .Borders(xlInsideHorizontal).Weight = xlHairline
    End With

End Sub

◇ ポイント

  • セル範囲(Range)を渡すとなにかやってくれるSubは便利
  • プロシージャ名は長くてOK(メインコードでもCallの1行になるだけ)
  • 特に日本語は短い文字で込めれる意味が英語の比ではない。前後のコードから中身を推測するプロシージャ名ではなく、本当に処理をすべてプロシージャ名に書けたりする
  • プロシージャ名に句読点も使える

特有のシート名などテキスト

◇ Before

Set wsデータ = Worksheets("くだもの売上データ(" & ws抽出シート.Range("C2") & "年度)")

◇ After

' メインコード
Set wsデータ = Worksheets(SheetNameくだもの売上データ(Range("C2")))
' プロシージャ
Function SheetNameくだもの売上データ(年度 As Long) As String
    SheetNameくだもの売上データ = "くだもの売上データ(" & 年度 & "年度)"
End Function

◇ ポイント
○○ & ( & パラメータ & ) & △△
みたいなテキストもFunctionにしておくと、何度も出てくるとき楽!

ユーザーの入力チェック

◇ Before

If ws抽出シート.Range("C2") = "" Then
    MsgBox "対象年度を入力して下さい。"
Else
    Dim ws As Worksheet
    Dim Is対象年度のシートが存在する As Boolean
    For Each ws In ThisWorkbook.Worksheets
        If ws.Name = "くだもの売上データ(" & ws抽出シート.Range("C2") & "年度)" Then
            Is対象年度のシートが存在する = True
        End If
    Next
    
    If Is対象年度のシートが存在する = False Then
        MsgBox "指定年度のデータがありません。"
    ElseIf ws抽出シート.Range("C3") = "" Then
        MsgBox "商品名を入力してください。"
    ElseIf Worksheets("くだもの売上データ(" & ws抽出シート.Range("C2") & "年度)").Columns(6) _
    .Find(ws抽出シート.Range("C3"), lookat:=xlWhole) Is Nothing Then
        MsgBox "入力された商品名はデータに登録されていません。"
    ElseIf ws抽出シート.Range("C4") = "" Then
        MsgBox "条件売上額を入力してください。"
    ElseIf IsNumeric(ws抽出シート.Range("C4")) = False Then
        MsgBox "売上額は数値で入力してください。"
    Else
        ' やっとここからメインコード開始

◇ After

' メインコード
If Isユーザーの入力がマクロの実行条件を満たす(ws抽出シート) = False Then Exit Sub

' たった1行のチェックの後すぐにメインコード開始
' プロシージャ
Function Isユーザーの入力がマクロの実行条件を満たす(ws抽出シート As Worksheet) As Boolean
   中身はBeforeと全く同じコードなので割愛

        MsgBox "売上額は数値で入力してください。"
    Else
        Isユーザーの入力がマクロの実行条件を満たす = True
        ' ↑メインコードを書いていた部分に「Function名=True」を書けば、
        ' このFunctionがTrueを返す⇒マクロ実行OK!という構図にできる
    End If

◇ ポイント

  • マクロの最初に「実行条件を確認」している部分は、外に切り出すのが簡単でおすすめ。
  • もともと冒頭にあるだけあって、ただの切り取り⇒貼り付けで最後にFunction名=Trueを付け足すだけでOK
  • 「メインマクロの開始直後からメインコードが始まるようになる」ので可読性が極端に上がる

簡単かつ効果が高い分割ポイントなので、
まずはここから実践してみるといいかもしれません。

ループの中身

◇ Before
※ 長いため割愛

◇ After

Dim R抽出シート As Long: R抽出シート = 7
Dim Rデータ As Long
For Rデータ = 2 To LastRデータ

    Call 行ごとの転記処理(wsデータ, Rデータ, ws抽出シート, R抽出シート)

Next

◇ ポイント
これだけで記事が数本かけるくらい奥が深い話なので触りだけ紹介します。

ループ処理の中身を丸ごとプロシージャに切り出すと、
メインコードがスッキリする上に、ロジックを考える負担が大分軽減します。

  • 繰り返し処理の仕様 = For Nextの書き方
  • 繰り返す中身の処理 = 実際の処理コード

を分けて考えることができるようになるということですね。

完成コードとまとめ

これですべてのプロシージャ分割が完了しました。

完成したコードがこちらになりますので、
元のコードと比べてみてください。


せっかくなのでサンプルファイルを見たり、適当なExcelに貼り付けて、
アコーディオン開閉のショートカットを使いながら見るといいかもしれません。

' メインプロシージャ
Sub くだもの売上データをシート指定条件で抽出する()

    Dim ws抽出シート As Worksheet: Set ws抽出シート = Worksheets("抽出シート")

    If Isユーザーの入力がマクロの実行条件を満たす(ws抽出シート) = False Then Exit Sub

    Call Excelの自動更新を停止する

    Call 指定行より下の行をすべて削除する(ws抽出シート, 7)

    Dim wsデータ As Worksheet
    Set wsデータ = Worksheets(SheetNameくだもの売上データ(Range("C2")))
    Dim LastRデータ As Long: LastRデータ = 最終行を取得する(wsデータ, 1)

    Dim R抽出シート As Long: R抽出シート = 7
    Dim Rデータ As Long
    For Rデータ = 2 To LastRデータ
        Call 行ごとの転記処理(wsデータ, Rデータ, ws抽出シート, R抽出シート)
    Next

    Call 指定エリアの外枠に実線、中枠に最細罫線を引く _
        (ws抽出シート.Range(ws抽出シート.Cells(7, 2), ws抽出シート.Cells(R抽出シート - 1, 5)))

    Call Excelの自動更新を再開する

    MsgBox "データの抽出を完了しました。"
End Sub

' 分割したプロシージャたち

Sub 行ごとの転記処理(wsデータ As Worksheet, Rデータ As Long, ws抽出シート As Worksheet, R抽出シート As Long)

    If wsデータ.Cells(Rデータ, 6) = ws抽出シート.Range("C3") _
    And wsデータ.Cells(Rデータ, 9) >= ws抽出シート.Range("C4") Then

        ws抽出シート.Cells(R抽出シート, 3) = wsデータ.Cells(Rデータ, 4)
        ws抽出シート.Cells(R抽出シート, 4) = wsデータ.Cells(Rデータ, 6)
        ws抽出シート.Cells(R抽出シート, 5) = wsデータ.Cells(Rデータ, 9)

        If wsデータ.Cells(Rデータ, 1) >= 4 Then
            ws抽出シート.Cells(R抽出シート, 2) _
                = DateSerial(ws抽出シート.Range("C2"), wsデータ.Cells(Rデータ, 1), wsデータ.Cells(Rデータ, 2))
        Else
            ws抽出シート.Cells(R抽出シート, 2) _
                = DateSerial(ws抽出シート.Range("C2") + 1, wsデータ.Cells(Rデータ, 1), wsデータ.Cells(Rデータ, 2))
        End If

        R抽出シート = R抽出シート + 1

    End If
End Sub

Function SheetNameくだもの売上データ(年度 As Long) As String
    SheetNameくだもの売上データ = "くだもの売上データ(" & 年度 & "年度)"
End Function

Function Isユーザーの入力がマクロの実行条件を満たす(ws抽出シート As Worksheet) As Boolean

    If ws抽出シート.Range("C2") = "" Then
        MsgBox "対象年度を入力して下さい。"
    Else

        If Isシートが存在する(ThisWorkbook, SheetNameくだもの売上データ(Range("C2"))) = False Then
            MsgBox "指定年度のデータがありません。"
        ElseIf ws抽出シート.Range("C3") = "" Then
            MsgBox "商品名を入力してください。"
        ElseIf Worksheets("くだもの売上データ(" & ws抽出シート.Range("C2") & "年度)").Columns(6) _
        .Find(ws抽出シート.Range("C3"), lookat:=xlWhole) Is Nothing Then
            MsgBox "入力された商品名はデータに登録されていません。"
        ElseIf ws抽出シート.Range("C4") = "" Then
            MsgBox "条件売上額を入力してください。"
        ElseIf IsNumeric(ws抽出シート.Range("C4")) = False Then
            MsgBox "売上額は数値で入力してください。"
        Else
            Isユーザーの入力がマクロの実行条件を満たす = True
        End If
    End If

End Function


' ここから下のコードは「汎用プロシージャ」のような名前のモジュールに移動すると便利です。

Sub Excelの自動更新を停止する()
    Application.ScreenUpdating = False
    Application.EnableEvents = False
    Application.Calculation = xlCalculationManual
    Application.Cursor = xlWait
End Sub

Sub Excelの自動更新を再開する()
    Application.ScreenUpdating = True
    Application.EnableEvents = True
    Application.Calculation = xlCalculationAutomatic
    Application.Cursor = xlDefault
End Sub

Sub 指定行より下の行をすべて削除する(ws指定シート As Worksheet, 削除開始行 As Long)
    ws指定シート.Range(ws指定シート.Rows(削除開始行), ws指定シート.Rows(ws指定シート.Rows.Count)).Delete
End Sub

Function 最終行を取得する(ws指定シート As Worksheet, 指定列 As Long) As Long
    最終行を取得する = ws指定シート.Cells(ws指定シート.Rows.Count, 指定列).End(xlUp).Row
End Function

Function Isシートが存在する(wb指定ブック As Workbook, 判定シート名 As String) As Boolean

    Dim ws As Worksheet
    For Each ws In wb指定ブック.Worksheets
        If ws.Name = 判定シート名 Then
            Isシートが存在する = True
            Exit Function
        End If
    Next

    Isシートが存在する = False
End Function

Sub 指定エリアの外枠に実線、中枠に最細罫線を引く(罫線設置エリア As Range)

    With 罫線設置エリア
        .Borders.LineStyle = True
        .Borders.Weight = xlThin
        .Borders(xlInsideVertical).Weight = xlHairline
        .Borders(xlInsideHorizontal).Weight = xlHairline
    End With

End Sub

総コード量は増えますが、
メインコードがものすごくすっきりしたのがわかりますね。


このコードになったことによって、

  • メインコードでは全体の流れがつかめる
  • 各プロシージャでは処理の詳細がわかる
  • 読みたい処理だけを細かく見て、それ以外の処理は読み飛ばすことができる。

というプロシージャ分割の恩恵が得られています。


このようにプロシージャ分割(Sub/Function)を活用すると、
コードのメンテナンス性を著しく向上することができます。


実際に実装するとなると最初は難しいかもしれませんが、
プロシージャ分割は「流れをつかむこと」が大事です。

「今自分は見出しを付けているんだ」ということを忘れずに、
簡単な部分から挑戦してみてください。


最後に今回のポイント集をまとめておきます。

  • Sub・Functionプロシージャを使うことは「処理のまとまりに見出しを付けて1行に要約する」こと
  • ショートカットトリオ「Shift+F2」「Ctrl+Shift+F2」「Ctrl + Space」
  • プロシージャ分割は「完成したコードを切り分ける」練習から
  • 見出しコメントをCallに書き換えるのがSub
  • 引数とはコピペコードの書き換え箇所
  • 読めるけど書けないコードをプロシージャに
  • Functionとはイコールの右側
  • Ifの中身にもFunction
  • 冒頭の実行条件チェックから始めるのが簡単&効果的

こんな感じになりますかね。


記事自体は大分長くなってしまいましたが、
このポイント集とサンプルのExcelブックさえ持っておけば、
復習には困らないと思います。


プロシージャ分割をマスターするのは大変ですが、これができるようになると、
処理の構造を考えてプログラミングすることができるようになります。

出来上がるマクロのメンテナンス性が上がるだけでなく、
コーディング自体が如実に上達すると思います。


あとは訓練あるのみ、ぜひ習得を目指してください!



最後になりますが、今回の記事をここまで読み切るにあたり、

プロシージャ名が英語だったらここまで読み切れただろうか?

と考えてみてください。


何度も書いてきた通り、プロシージャ分割は「要約した見出し」を使って、
「全体の流れをつかむ」ことです。

この「要約」と「流れ」こそ母国語の力が真に生きる部分であり、
プロシージャ分割は、母国語で学ぶべきスキルと思っています。


最終的に英語変数/関数を使いたいと思っている方も、
「プロシージャ分割」や、せめて「クラス」くらいは母国語で学んで、
そのあとで英訳したって遅くはないです。


日本語識別子(変数名/関数名)の使用を、是非ご検討ください。


超長文読了お疲れ様でした!

おまけ:汎用関数は別のモジュールへ

完成コード内に

' ここから下のコードは「汎用プロシージャ」のような名前のモジュールに移動すると便利です。

というコメントを書いておきました。

このコメントより下のプロシージャたちは、
どんなマクロでも使える汎用プロシージャです。


当然便利なのは間違いないんですが、これを作り始めると悩むのが、
このプロシージャをどこから持ってくるのか
問題です。


いちいち前回使っていた場所からコピペしてくるのでは、
結局汎用化した意味が薄れますからね。


この管理方法としては、
汎用関数は一つのモジュールにまとめてもっておき、
 それを保存するExcelブックを決めておく

のがおすすめです。


「汎用プロシージャ集.xlsm」というブックの「汎用プロシージャ」というモジュールに汎用プロシージャを全部書いておけば、
使うときはそのモジュールをドラッグ&ドロップするだけで使えるようになります。


そうすると使わない関数まで大量に持ってきてしまいますが、
別にそれで困ることはありません。

私自身、「200以上の関数をもってきて使うのは最終行だけ」とかやりますが、
それで困ったことはほとんどないです。


また、この「保存するExcelブック」には、
個人用マクロブック」というものがおすすめです。

Excelを開くと必ず裏で開かれるブックなのでわざわざ開く手間がなくなります。


詳しくはこちらの記事に書いてありますので、
よろしければこちらもご覧ください。

※ 前半はプロシージャ分割のやり方で内容が本記事と被るので、
個人用マクロブックの使い方部分から読めばOKです。

www.limecode.jp