【エクセルVBA】マクロを部品化(モジュール化)して「楽」しよう

ある程度マクロ作成に慣れてくると、こんなことありませんか?

  • いま作ってるマクロって、最近似たようなもの作った気がするなぁ。
  • この処理も前に作成したマクロのあの部分と同じだなぁ。
    コピペしたいけど、どこに置いたっけ?
  • 「最終行の取得」や「ファイル一覧の取得」などのよく使う処理って、
    マクロ作成するたびに毎回同じコード書いてるなぁ。

確かにそういうこと、よくあるわね。

前のコードをコピペするにしても、結局同じコード書いてるわけだから、
なんだか効率悪いし。

そういう時は、よく使う処理を
「Subプロシージャ」や「Functionプロシージャ」で「部品化」して

「別モジュール」で管理

しておいて、必要なところから呼び出せるようにしておくのがおすすめね。

ということで「部品化して楽しよう」について説明していきます。

部品化とは

「部品化」とは、よく使う処理や同じような処理を部品として外出し(独立化)しておき、その処理を使いたいところから呼び出して汎用的な再利用ができるようにすることです。

ここでは「部品化」と表現していますが、一般的には「モジュール化」といわれることの方が多いかもしれません。意味は同じです。

部品化することの利点

部品化することには、以下のような利点があります。

1.再利用できる(生産性の向上)

同じ処理を新たにコーディングしないので、時間の節約ができます。

また、部品化されたものは少なくとも一度は使用され、ある程度精査されているわけですから、「書く」ということの時間だけでなく、「処理内容の確認時間」も節約できます。

2.同じことを何度も調べないで済む(生産性の向上)

マクロを作成する際に、同じことを何度も調べたりしていませんか?

今の時代、ググらずにコーディングすることはまずないかと思いますが、同じことを何度も調べるのは明らかに時間の無駄ですよね。

部品化しておき、それが何の処理なのかを明確にしておけば、使う側は呼び出すだけでいいので、これも時間の短縮になります。

3.コーディングのミスを減らせる(精度の向上)

新たにコーディングしないので、それだけ「ミスを発生させる機会が減る」わけです。

わざわざ部品化しないでも「コピペ」すれば、間違えることはないのでは?という意見もあるかもしれませんが、そうとも言えません。

コピペする際に1行だけコピペを忘れたとかなかったでしょうか。
コピペ元に修正が入った場合、同じ修正をコピペ先にもすることを忘れたことなかったでしょうか。
他の人が修正した場合、修正箇所が複数あることに気づきずらいのではないでしょうか。

作った本人でさえも、数日もすれば忘れてしまうことでしょう。

これはコピペしなければ避けられるミスです。

「コードを書くということは、その分だけバグが入る余地を増やしている」と考えるのがよいと思います。

4.コードの重複をなくせる(精度の向上)

「よく使う処理」ということは、コード上に「何度も出てくる処理」とも言えるわけです。

「何度も出てくる」とは「重複する」ということ。
重複コードはバグを発生させる温床となります。

同じようなコードが2つ以上あったら、それは「部品化すべきところ」です。

5.コードが見やすくなる、理解しやすくなる(可読性の向上)

部品を使う側は呼び出しのコードを書くだけなので、メイン処理のSubプロシージャを短くできます。

ソースコードが全体的にスッキリして見やすくなります。

他の人が見ても、比較的理解しやすいコーディングになると思います。

6.メンテナンスしやすい(保守性の向上)

ある処理にバグがあることが分かった場合、その処理がソースコードの3カ所に書かれていたら、その全てに同じ修正を入れないといけません。

このとき起こりやすいのが「1カ所だけ修正するの忘れてしまった」という修正漏れです。

この3カ所に散らばっている処理を部品化して1カ所にしていれば、修正は1カ所だけで済み、修正漏れも発生しないわけです。

7.蓄積できる(生産性の向上)

部品化を進めていくことで、よく使う処理をだんだんと蓄積していくことができます。

新しく作成するマクロでは、その蓄積された部品を呼び出すだけでよくなります。

部品化は処理を外出しするので、少しだけ手間がかかるのは事実だと思います。
「数行だから部品化しないでも、また書けばいいや」と思ってしまうのもわかります。

ですが、数行のものでも、一週間後には忘れていることもあるのではないでしょうか。

そうすると、毎回同じようなコードを書き続けることになります。

部品化してみる

部品化の例

以下のような処理内容を例として、部品化をしてみます。

Sheet2とSheet3のデータをコピーしてSheet1に貼り付ける。

データ貼り付けイメージ

上記データは、ダミー情報を自動生成するWebサービス「個人情報テストデータジェネレーター」を使用して得たダミーデータです。

コードの書き方はいろいろありますが、処理をそのまま書き連ねると、以下のようなコードになります。

部品化前
Sub Main()
    '----------------------------- ブロック1 -----------------------------
    '画面表示と自動計算の停止
    Application.ScreenUpdating = False
    Application.Calculation = xlCalculationManual

    '----------------------------- ブロック2 -----------------------------
    'Sheet1の準備
    Dim ws_1 As Worksheet
    Set ws_1 = ThisWorkbook.Worksheets("Sheet1")
    Dim endRow_1 As Long
    
    '----------------------------- ブロック3 -----------------------------
    'Sheet2の準備
    Dim ws_2 As Worksheet
    Set ws_2 = ThisWorkbook.Worksheets("Sheet2")
    
    'Sheet2の最終行を取得
    Dim endRow_2 As Long
    endRow_2 = ws_2.Cells(ws_2.Rows.Count, 1).End(xlUp).Row
    
    'Sheet2の最終列を取得
    Dim endCol_2 As Long
    endCol_2 = ws_2.Cells(1, ws_2.Columns.Count).End(xlToLeft).Column

    'Sheet2のデータ範囲を取得
    Dim targetRng_2 As Range
    Set targetRng_2 = ws_2.Range(ws_2.Cells(2, 1), ws_2.Cells(endRow_2, endCol_2))

    'Sheet1の最終行を取得
    endRow_1 = ws_1.Cells(ws_1.Rows.Count, 1).End(xlUp).Row
    
    'Sheet2のデータ範囲をコピーして、Sheet1に貼り付け
    targetRng_2.Copy Destination:=ws_1.Cells(endRow_1 + 1, 1)
    
    '----------------------------- ブロック4 -----------------------------
    'Sheet3の準備
    Dim ws_3 As Worksheet
    Set ws_3 = ThisWorkbook.Worksheets("Sheet3")
    
    'Sheet3の最終行を取得
    Dim endRow_3 As Long
    endRow_3 = ws_3.Cells(ws_3.Rows.Count, 1).End(xlUp).Row
    
    'Sheet3の最終列を取得
    Dim endCol_3 As Long
    endCol_3 = ws_3.Cells(1, ws_3.Columns.Count).End(xlToLeft).Column

    'Sheet3のデータ範囲を取得
    Dim targetRng_3 As Range
    Set targetRng_3 = ws_3.Range(ws_3.Cells(2, 1), ws_3.Cells(endRow_3, endCol_3))

    'Sheet1の最終行を取得
    endRow_1 = ws_1.Cells(ws_1.Rows.Count, 1).End(xlUp).Row
    
    'Sheet3のデータ範囲をコピーして、Sheet1に貼り付け
    targetRng_3.Copy Destination:=ws_1.Cells(endRow_1 + 1, 1)

    '----------------------------- ブロック5 -----------------------------
    '画面表示と自動計算の設定を元に戻す
    Application.ScreenUpdating = True
    Application.Calculation = xlCalculationAutomatic
End Sub

分かりやすいように、ブロック1~5に分けました。

問題点を挙げてみると、こんなところでしょうか。

  1. Subプロシージャが長すぎる。(長くても20行~30行以内を推奨)
  2. 対象とするシートが違うだけの全く同じ処理が、「ブロック3」と「ブロック4」に重複して書かれている。
  3. 「よく使う処理」が部品化されていない。

この記事のテーマである「部品化」に注目して挙げています。
数値がベタ書きされている(マジックナンバー)などの問題は、ここでは除外します。

ソースコードの説明についても割愛します。

また、以降で紹介する部品化例は、あくまでも私見によるものです。こうしなければいけないというものではありませんので、ご了承ください。

部品化(その1) 同じ処理の部品化

まずは「問題点2」について変更をいれてみます。同じ処理の部分は、部品化すべきところです。

Subプロシージャとして外出ししてみましょう。

Sub Main()
    '----------------------------- ブロック1 -----------------------------
    '画面表示と自動計算の停止
    Application.ScreenUpdating = False
    Application.Calculation = xlCalculationManual

    '----------------------------- ブロック2 -----------------------------
    'Sheet1の準備
    Dim ws_1 As Worksheet
    Set ws_1 = ThisWorkbook.Worksheets("Sheet1")
    
    '----------------------------- ブロック3 -----------------------------
    'Sheet2の準備
    Dim ws_2 As Worksheet
    Set ws_2 = ThisWorkbook.Worksheets("Sheet2")
    Call CopyData(ws_2, ws_1)
    
    '----------------------------- ブロック4 -----------------------------
    'Sheet3の準備
    Dim ws_3 As Worksheet
    Set ws_3 = ThisWorkbook.Worksheets("Sheet3")
    Call CopyData(ws_3, ws_1)

    '----------------------------- ブロック5 -----------------------------
    '画面表示と自動計算の設定を元に戻す
    Application.ScreenUpdating = True
    Application.Calculation = xlCalculationAutomatic
End Sub

'----------------------------------------------
'コピーシートから貼り付けシートにデータを貼り付ける
'----------------------------------------------
Sub CopyData(copyWs As Worksheet, pasteWs As Worksheet)
    'コピーシートの最終行を取得
    Dim endRow As Long
    endRow = copyWs.Cells(copyWs.Rows.Count, 1).End(xlUp).Row
    
    'コピーシートの最終列を取得
    Dim endCol As Long
    endCol = copyWs.Cells(1, copyWs.Columns.Count).End(xlToLeft).Column

    'コピーシートのデータ範囲を取得
    Dim targetRng As Range
    Set targetRng = copyWs.Range(copyWs.Cells(2, 1), copyWs.Cells(endRow, endCol))

    '貼り付けシートの最終行を取得
    Dim endRow_pasteWs As Long
    endRow_pasteWs = pasteWs.Cells(pasteWs.Rows.Count, 1).End(xlUp).Row
    
    'コピーシートのデータ範囲をコピーして、貼り付けシートに貼り付け
    targetRng.Copy Destination:=pasteWs.Cells(endRow_pasteWs + 1, 1)
End Sub

「ブロック3」と「ブロック4」の同じ処理を、Subプロシージャ「CopyData」として外出ししました。

改善された点として、以下が挙げられます。

  • 「問題点2」が解消され、スッキリして見やすくなった。(可読性の向上)
  • 「ブロック3」と「ブロック4」が、同じ処理をしていることが理解しやすくなった。
    部品化前は一見したところ同じ処理をしているように見えるが、もしかしたらどこかが違うかもしれない。同じかどうか確かめるには、処理を1行ずつ追わないといけないが、部品化後はその必要がない。(可読性の向上)
  • もし「Sheet4からもデータコピーする」ことになった場合、Sheet4に対して「CopyData」を呼び出すだけの分かりやすく、簡単な修正で済む。(生産性の向上)(保守性の向上)
部品化(その2) よく使う処理の部品化

続いて「問題点3」の「よく使う処理」を外出ししてみます。

ここでは、以下の2点を外出しします。

  1. 画面表示と自動計算の設定
  2. 最終行、最終列の取得
Sub Main()
    '----------------------------- ブロック1 -----------------------------
    Call StartSetting

    '----------------------------- ブロック2 -----------------------------
    Dim ws_1 As Worksheet
    Set ws_1 = ThisWorkbook.Worksheets("Sheet1")
    
    '----------------------------- ブロック3 -----------------------------
    Dim ws_2 As Worksheet
    Set ws_2 = ThisWorkbook.Worksheets("Sheet2")
    Call CopyData(ws_2, ws_1)
    
    '----------------------------- ブロック4 -----------------------------
    Dim ws_3 As Worksheet
    Set ws_3 = ThisWorkbook.Worksheets("Sheet3")
    Call CopyData(ws_3, ws_1)

    '----------------------------- ブロック5 -----------------------------
    Call EndSetting

End Sub

'----------------------------------------------
'コピーシートから貼り付けシートにデータを貼り付ける
'----------------------------------------------
Sub CopyData(copyWs As Worksheet, pasteWs As Worksheet)
    'コピーシートのデータ範囲を取得
    Dim targetRng As Range
    Set targetRng = copyWs.Range(copyWs.Cells(2, 1), _
                                 copyWs.Cells(GetEndRow(copyWs, 1), GetEndCol(copyWs, 1)))
    
    'コピーシートのデータ範囲をコピーして、貼り付けシートに貼り付け
    targetRng.Copy Destination:=pasteWs.Cells(GetEndRow(pasteWs, 1) + 1, 1)
End Sub

'----------------------------------------------
'画面表示と自動計算の停止
'----------------------------------------------
Sub StartSetting()
    Application.ScreenUpdating = False
    Application.Calculation = xlCalculationManual
End Sub

'----------------------------------------------
'画面表示と自動計算の設定を元に戻す
'----------------------------------------------
Sub EndSetting()
    Application.ScreenUpdating = True
    Application.Calculation = xlCalculationAutomatic
End Sub

'----------------------------------------------
'最終行を取得
'----------------------------------------------
Function GetEndRow(targetWs As Worksheet, targetCol As Long) As Long
    GetEndRow = targetWs.Cells(targetWs.Rows.Count, targetCol).End(xlUp).Row
End Function

'----------------------------------------------
'最終列を取得
'----------------------------------------------
Function GetEndCol(targetWs As Worksheet, targetRow As Long) As Long
    GetEndCol = targetWs.Cells(targetRow, targetWs.Columns.Count).End(xlToLeft).Column
End Function

以下のような変更を加えています。

  • 画面表示と自動計算の設定をSubプロシージャ「StartSetting」「EndSetting」とし、「Main」から呼び出すように変更。
  • 最終行と最終列の取得をFunctionプロシージャ「GetEndRow」「GetEndCol」とし、「CopyData」から呼び出すように変更。
  • 「CopyData」の最終行、最終列取得のコード部分が「GetEndRow」「GetEndCol」と短くなったので、変数を用意せず、使用箇所から直接呼び出すように変更。

改善された点として、以下が挙げられます。

  • 呼び出しの1行を書くだけで、他のプロシージャからも使える。(生産性の向上)(精度の向上)
  • 各プロシージャが20行~30行以内に納まり、「問題点1」が解消できている。
  • 外出ししたプロシージャ名で処理内容を簡潔に表わすことにより、呼び出し元でコメントを付けなくても理解しやすい。(可読性の向上)

部品化する際の注意点

例を通して説明してきましたが、「部品化」する際の注意点があります。

他にもありそうですが、個人的によく気にしている点を挙げておきます。

「よく使う処理」を部品化する場合、個別のデータや処理に依存しない、汎用的な使用ができるようなプロシージャにする。

外出ししたプロシージャを、個別のデータや処理に依存するようなものにしてしまうと、再利用の利点を生かしきれなくなってしまいます。

プロシージャ名は、処理内容が明確で分かりやすく、簡潔なものにする。

再利用する際に「これどんな処理だっけ」とならないようにするためと、コードの可読性の意味からも、処理内容が分かりやすい名前にしておきます。

「部品化」がうまくいかなかった場合のために、コードのバックアップを取っておく。

いきなり修正を入れてしまうと、うまくいかなかった時に動かなくなったりするので、元に戻せるように部分的にでもバックアップしておくことをお勧めします。

自分用の部品集を作成する

「よく使う処理」を部品化したものは、他のマクロからも呼び出せるように、「部品集」として蓄積しておきましょう。

1.新しいモジュールを追加する

外出ししたプロシージャを別モジュールにコピーして、後で使えるようにします。

まず、標準モジュールを追加します。

標準モジュールの追加

モジュール名は変えても変えなくても構いません。私は「Util」という名前にしています。

2.部品化するコードをコピーする

部品化するコードをコピーします。

他のモジュールからも呼び出せるように「Public」を付けてください。

また、「Public」にするとボタンにマクロ登録する際に、登録候補として出てきてしまうので、コードの先頭に「Option Private Module」の一行を追加しておくとよいです。

コーディング補足
3.追加したモジュールをエクスポートして、部品集として管理する

追加したモジュールをエクスポートします。

エクスポート
4.新しいマクロを作成する際は、部品集をインポートする

新しいマクロを作成したら、エクスポートした部品集をインポートします。

インポート

これで、部品集のプロシージャを新しいマクロで呼び出して使えば、新たにコーディングしないで済みます。

このとき、部品集を修正したり、新たに部品を追加した場合は、忘れずにエクスポートして、最新版として保持できるようにしてください。

また、ファイルが先祖返りしないように、ファイル名に日時など付けて履歴管理してもいいかもしれません。

まとめ

長くなりましたが、部品化しておくと、様々なメリットが得られると思います。

処理が分割されるので、はじめは読みずらいことがあるかもしれませんが、そのうち部品化せずにはいられなくなると思います。

ここで紹介した例は簡単なものですが、コードが大きくなるほどメリットも大きくなります。

部品化を進めて「楽」しましょう。