ExcelVBA練習問題シリーズ#2「注文書をデータ取込」の、
マクロ①②+挑戦ポイントにすべて対応した完全解答版です。
もちろんこれが正解という訳ではなく、いろいろなやり方あると思いますが、
ひとつの解答としてご参考ください。
◇ 出題ページはこちら
www.limecode.jp
◇ 解答ページはこちら
VBA問題#2「注文書をデータ取込」マクロ①解答
VBA問題#2「注文書をデータ取込」マクロ②解答
VBA問題#2「注文書をデータ取込」完成版解答(挑戦ポイント制覇)【本ページ】
ソースコード
標準モジュール:Df_シートレイアウト
Option Explicit ' 販売データ Public Const Adrs販売データ_年度 = "F2" Public Const R1st販売データ = 5 Public Const C1st販売データ = 2 Public Enum CNo販売データ No = C1st販売データ 購入者 月 日 品物 価格 個数 売上 取込日 End Enum Public Const CLast販売データ = CNo販売データ.取込日 ' 注文書 Public Const Adrs注文書_購入者 = "C5" Public Const Adrs注文書_注文日 = "C7" Public Const Adrs注文書_合計税別 = "F22" Public Const Adrs注文書_消費税 = "F23" Public Const Adrs注文書_合計税込 = "F24" Public Const R1st注文書 = 12 Public Const RLast注文書 = 21 Public Const C1st注文書 = 2 Public Enum CNo注文書 No = C1st注文書 品物 価格 個数 金額 End Enum Public Const CLast注文書 = CNo注文書.金額 ' 実行シート Public Const Adrs実行シート_ファイルパス = "C4" Public Const Adrs実行シート_フォルダパス = "C9" Public Const R1st取込ログ = 12 Public Const C1st取込ログ = 2 Public Enum CNo取込ログ No = C1st取込ログ 取込ファイル名 結果 End Enum Public Const CLast取込ログ = CNo取込ログ.結果 Function Is注文書シート(判定シート As Worksheet) As Boolean Is注文書シート = 判定シート.Range("B2") = "注 文 書" _ And 判定シート.Range("D24") = "合計(税込)" End Function
標準モジュール:Pr0_注文書取込_共通コード
Option Explicit ' 注文書 → 販売データ Sub 指定の注文書シートを販売データに取り込む(ws注文書 As Worksheet) ' 出力第1行(最終行+1)と出力No(最大値+1)を取得 Dim R_出力 As Long: R_出力 = Get最終行(WS販売データ) + 1 Dim 出力No As Long: 出力No = WorksheetFunction.Max(WS販売データ.Columns(CNo販売データ.No)) + 1 ' 注文書の表部分をループ Dim R_取込 As Long For R_取込 = R1st注文書 To RLast注文書 ' 金額が0でない行を取込 If ws注文書.Cells(R_取込, CNo注文書.金額) <> 0 Then WS販売データ.Cells(R_出力, CNo販売データ.No) = 出力No WS販売データ.Cells(R_出力, CNo販売データ.購入者) = ws注文書.Range(Adrs注文書_購入者) WS販売データ.Cells(R_出力, CNo販売データ.月) = Month(ws注文書.Range(Adrs注文書_注文日)) WS販売データ.Cells(R_出力, CNo販売データ.日) = Day(ws注文書.Range(Adrs注文書_注文日)) WS販売データ.Cells(R_出力, CNo販売データ.品物) = ws注文書.Cells(R_取込, CNo注文書.品物) WS販売データ.Cells(R_出力, CNo販売データ.価格) = ws注文書.Cells(R_取込, CNo注文書.価格) WS販売データ.Cells(R_出力, CNo販売データ.個数) = ws注文書.Cells(R_取込, CNo注文書.個数) WS販売データ.Cells(R_出力, CNo販売データ.売上) = ws注文書.Cells(R_取込, CNo注文書.金額) WS販売データ.Cells(R_出力, CNo販売データ.取込日) = Date ' 出力行とNoを更新 R_出力 = R_出力 + 1 出力No = 出力No + 1 End If Next ' 注文書の表部分をループ End Sub ' 取込済判定(同日同購入者で判定) Function Is指定注文書が販売データに取込済(ws注文書 As Worksheet) As Boolean ' そもそも年度が異なればExit If WS販売データ.Range(Adrs販売データ_年度) <> Get年度(ws注文書.Range(Adrs注文書_注文日)) Then Exit Function ' 判定キーの取得 Dim key月 As Long: key月 = Month(ws注文書.Range(Adrs注文書_注文日)) Dim key日 As Long: key日 = Day(ws注文書.Range(Adrs注文書_注文日)) Dim key購入者 As String: key購入者 = ws注文書.Range(Adrs注文書_購入者) ' 全キーが一致するデータがあるか判定 Dim R As Long For R = R1st販売データ To Get最終行(WS販売データ) If WS販売データ.Cells(R, CNo販売データ.月) = key月 _ And WS販売データ.Cells(R, CNo販売データ.日) = key日 _ And WS販売データ.Cells(R, CNo販売データ.購入者) = key購入者 Then Is指定注文書が販売データに取込済 = True Exit Function End If Next End Function
標準モジュール:Pr1_注文書取込_ファイル指定
Option Explicit ' マクロ① 指定パスの注文書を取込 Sub 指定した注文書ファイルを販売データに取り込む() ' 取込ファイルの存在・ファイル形式チェック Dim path取込ファイル As String: path取込ファイル = WS実行シート.Range(Adrs実行シート_ファイルパス) Dim 取込ファイル名 As String: 取込ファイル名 = Dir(path取込ファイル) If 取込ファイル名 = "" Then MsgBox "指定したファイルが存在しません。": Exit Sub If Right(取込ファイル名, 5) <> ".xlsx" Then MsgBox "指定ファイルがxlsxファイルではありません。": Exit Sub ' 対象ブックを開く Dim path保存フォルダ As String: path保存フォルダ = Replace(path取込ファイル, "\" & 取込ファイル名, "") Dim wb取込ファイル As Workbook Set wb取込ファイル = ブックを開いていなければ開く(path保存フォルダ, 取込ファイル名) ' シートの存在チェック If Isシートが存在する("注文書", wb取込ファイル) = False Then MsgBox "注文書シートがないファイルです。": Exit Sub Dim ws注文書 As Worksheet: Set ws注文書 = wb取込ファイル.Worksheets("注文書") ' シートのレイアウトチェック If Is注文書シート(ws注文書) = False Then MsgBox "注文書シートのレイアウトが異なります。": Exit Sub ' 対象年度が異なれば取込を停止 If WS販売データ.Range(Adrs販売データ_年度) <> Get年度(ws注文書.Range(Adrs注文書_注文日)) Then MsgBox "注文書の年度が異なります。": Exit Sub ' 取込済判定 If Is指定注文書が販売データに取込済(ws注文書) Then MsgBox "同日同購入者のデータが既に存在します。": Exit Sub Dim R_第1出力行 As Long: R_第1出力行 = Get最終行(WS販売データ) + 1 ' ■ 取込メインプロシージャを実行 Call 指定の注文書シートを販売データに取り込む(ws注文書) Dim R_最終出力行 As Long: R_最終出力行 = Get最終行(WS販売データ) WS販売データ.Activate WS販売データ.Range(WS販売データ.Rows(R_第1出力行) _ , WS販売データ.Rows(R_最終出力行)).Select MsgBox "データの取込を完了しました。" End Sub
標準モジュール:Pr2_注文書取込_フォルダ一括
Option Explicit ' マクロ② 指定フォルダ内の全注文書を取込 Sub 指定フォルダ内にあるすべての注文書ファイルを販売データに取り込む() ' 取込ログをクリア WS実行シート.Range(WS実行シート.Rows(R1st取込ログ), WS実行シート.Rows(Get最終行(WS実行シート))).Value = "" ' 取込フォルダの存在チェック Dim Path取込フォルダ As String: Path取込フォルダ = WS実行シート.Range(Adrs実行シート_フォルダパス) If Dir(Path取込フォルダ, vbDirectory) = "" Then MsgBox "指定したフォルダが存在しません。": Exit Sub ' 処理後の移動先フォルダを作成 Dim Path完了フォルダ As String: Path完了フォルダ = Path取込フォルダ & "\" & Format(Date, "yyyymmdd") & "取込済" Dim Path除外フォルダ As String: Path除外フォルダ = Path取込フォルダ & "\除外ファイル" If Dir(Path完了フォルダ, vbDirectory) = "" Then MkDir Path完了フォルダ If Dir(Path除外フォルダ, vbDirectory) = "" Then MkDir Path除外フォルダ ' ファイルの存在チェック Dim dirファイル名 As String: dirファイル名 = Dir(Path取込フォルダ & "\*.xlsx") If dirファイル名 = "" Then MsgBox "指定フォルダ内にxlsxファイルがひとつもありません。": Exit Sub ' 第1出力列を記憶 Dim R_第1出力行 As Long: R_第1出力行 = Get最終行(WS販売データ) + 1 ' フォルダ内のすべてのxlsxファイルを取得 Dim 取込ファイルNo As Long: 取込ファイルNo = 1 Dim R_ログ As Long: R_ログ = R1st取込ログ Do While dirファイル名 <> "" ' ファイル名とNoをログに記録 WS実行シート.Cells(R_ログ, CNo取込ログ.No) = 取込ファイルNo WS実行シート.Cells(R_ログ, CNo取込ログ.取込ファイル名) = dirファイル名 ' 注文書シートの取得 On Error Resume Next Dim wb取込ファイル As Workbook Set wb取込ファイル = ブックを開いていなければ開く(Path取込フォルダ, dirファイル名) Dim ws注文書 As Worksheet: Set ws注文書 = wb取込ファイル.Worksheets("注文書") On Error GoTo 0 ' 注文書シートがない If ws注文書 Is Nothing Then WS実行シート.Cells(R_ログ, CNo取込ログ.結果) = "注文書シートなし" ' 注文書のレイアウトが不一致 ElseIf Is注文書シート(ws注文書) = False Then WS実行シート.Cells(R_ログ, CNo取込ログ.結果) = "シートレイアウト不一致" ' 対象年度でない ElseIf WS販売データ.Range(Adrs販売データ_年度) <> Get年度(ws注文書.Range(Adrs注文書_注文日)) Then WS実行シート.Cells(R_ログ, CNo取込ログ.結果) = "データ年度不一致" ' 取込済 ElseIf Is指定注文書が販売データに取込済(ws注文書) Then WS実行シート.Cells(R_ログ, CNo取込ログ.結果) = "取込済の注文書" ' ■ 取込を実行 Else Call 指定の注文書シートを販売データに取り込む(ws注文書) WS実行シート.Cells(R_ログ, CNo取込ログ.結果) = "完了" End If ' ファイルを開いていたら閉じる If Not wb取込ファイル Is Nothing Then wb取込ファイル.Close False ' 結果が「完了」かどうかで判定 If WS実行シート.Cells(R_ログ, CNo取込ログ.結果) = "完了" Then Name Path取込フォルダ & "\" & dirファイル名 As Path完了フォルダ & "\" & dirファイル名 Else Name Path取込フォルダ & "\" & dirファイル名 As Path除外フォルダ & "\" & dirファイル名 End If ' 次のファイルへ 取込ファイルNo = 取込ファイルNo + 1 R_ログ = R_ログ + 1 dirファイル名 = Dir() Loop ' フォルダ内のすべてのxlsxファイルを取得 Dim R_最終出力行 As Long: R_最終出力行 = Get最終行(WS販売データ) WS販売データ.Activate WS販売データ.Range(WS販売データ.Rows(R_第1出力行) _ , WS販売データ.Rows(R_最終出力行)).Select MsgBox "データの取込を完了しました。" End Sub ' フォルダのダイアログ選択 Sub フォルダを選択する() WS実行シート.Range("C9") = GetPathダイアログボックスでフォルダを選択する(ThisWorkbook.Path) End Sub Function GetPathダイアログボックスでフォルダを選択する(Optional Path初期表示フォルダ As String = "") As String With Application.FileDialog(msoFileDialogFolderPicker) If Path初期表示フォルダ <> "" Then .InitialFileName = Path初期表示フォルダ If .Show Then: GetPathダイアログボックスでフォルダを選択する = .SelectedItems(1) End With End Function
標準モジュール:Ut_汎用関数
Option Explicit ' 最終行の取得 Function Get最終行(指定シート As Worksheet, Optional 列番号 As Long = -1) As Long With 指定シート ' 列番号を省略時はシートの最終行 If 列番号 = -1 Then Get最終行 = .UsedRange.Rows.Count + .UsedRange.Row - 1 ' 列番号を指定時は列の最終行 Else Get最終行 = .Cells(.Rows.Count, 列番号).End(xlUp).Row End If End With End Function ' ブックを開いていなければ開く Function ブックを開いていなければ開く(フォルダパス As String, ブック名 As String) As Workbook If Isブックを開いている(ブック名) Then Set ブックを開いていなければ開く = Workbooks(ブック名) Else On Error Resume Next Set ブックを開いていなければ開く = Workbooks.Open(フォルダパス & "\" & ブック名) On Error GoTo 0 End If End Function ' ブックのOpenチェック Function Isブックを開いている(判定ブック名 As String) As Boolean ' 開いているすべてのブックを走査 Dim wb As Workbook For Each wb In Workbooks ' ブック名が一致したらTrueを返してExit If wb.Name = 判定ブック名 Then Isブックを開いている = True Exit Function End If Next End Function ' シートの存在チェック Function Isシートが存在する(判定シート名 As String, 指定ブック As Workbook) As Boolean ' ブック内の全シートを走査 Dim ws As Worksheet For Each ws In 指定ブック.Worksheets ' シート名が一致したらTrueを返してExit If ws.Name = 判定シート名 Then Isシートが存在する = True Exit Function End If Next End Function ' 年度の取得 Function Get年度(ByVal 日付 As Date) As Long If Month(日付) >= 4 Then Get年度 = Year(日付) Else Get年度 = Year(日付) - 1 End If End Function
解説
処理の基本部分についての解説は、
マクロ①②の解答で詳しく行っていますのでそちらをご参考ください。
VBA問題#2「注文書をデータ取込」解答(マクロ①)
VBA問題#2「注文書をデータ取込」解答(マクロ②)
本記事では「挑戦ポイント」への対応内容を解説していきます。
コードを読む際は、上記マクロ①②の解答ファイルと見比べることで、
コードをどのように改良したかがより分かりやすくなると思います。
よろしければ横に並べてご覧ください。
レイアウト変更への対応を考慮した設計
【挑戦ポイント】
販売データの列の変更や、領収書のレイアウトの変更に対応できるよう設計してみてください。
こちらの課題、いわゆる「メンテナンス性の向上」については、
シートレイアウトをEnum・定数で定義しひとつのモジュールにまとめました。
軽微な変更(行列の挿入削除程度)であれば、
「Df_シートレイアウト」モジュールを変更するだけでマクロを改修できます。
簡単に解説しますと、Enum・定数を使用したことで、
WS販売データ.Cells(R_出力, 9) = ws注文書.Cells(R_取込, 6) ' ⇩ Enumと定数を使って書き替え WS販売データ.Cells(R_出力, CNo販売データ.売上) = ws注文書.Cells(R_取込, CNo注文書.金額)
こんな風にコードが書き換わっています。
まずはコードが読めるようになっているというのが大きな利点ですが、
さらに強力なメリットとして、列の追加削除対応を瞬殺できるようになります。
例えば「購入者」列の右に「住所」列を追加するとしましょう。
Enumなしの場合は、Cells中身の「9」を「10」に書き換える作業を、
すべてコードから探して行う必要があります。
しかも注文書の方に出てくる数字を増やしてはいけませんので、
機械的に書き換えていくわけにもいかず、すべてのCellsをよく見なければいけません。
この大変さを味わったことがある人は多いはず。
しかしEnumを用いている場合は、
Enumの設定箇所を↓のように書き換えるだけで改修が完了してしまいます。
Public Const C1st販売データ = 2 Public Enum CNo販売データ No = C1st販売データ ' ここが2 購入者 ' 以下Enumの自動連番機能で3,4,…と進む 住所 ' ← これをEnumに追加。4となる 月 ' ← ここから下は5,6,…と自動で+1されるので以下対応は不要! 日 品物 価格 個数 売上 ' ← 9が10にしっかり変わっている! 取込日 End Enum
素晴らしい機能ですよね!
列番号をいちいち打ち換える地獄のような作業が、
列名をひとつ書くだけで済むようになっています。
メンテナンス性を考えた時、これほど簡単で強力な機能はなかなかありませんので、
まだ使っていなかった方は是非一度使ってみて下さい。
Enumについては詳しく書いた記事がありますので、
興味がる方は是非ご覧下さい。
www.limecode.jp
またEnumの勉強会のYoutube配信アーカイブもありますので、
こちらもあわせてご活用いただければと思います。
Subプロシージャの分割
【挑戦ポイント】
マクロ1とマクロ2で同じコードを使えるように「Subプロシージャ」を分割してみてください。
こちらは「注文書1ファイル → 販売データ」のメイン処理部をプロシージャにしました。
マクロ1とマクロ2を1つのプロシージャで実装できているのも良さですが、
加えて両マクロともメインコードが簡潔になっているのも大きいですね。
特にマクロ2は「すべてのファイルを判定→開いて→処理→閉じる」というコードなのですが、
処理がCall1行になることでフォルダ走査のDoループ文が見渡しやすくなっています。
こういった長大なループを持つコードにおいては、
プロシージャ分割のメリット「処理に見出しをつけて1行にする」が活きてきます。
プロシージャ分割のメリットは「部品の共通化」だけではありませんので、
この使い方も意識していって下さい。
あとは全体的な話ですが、分割したプロシージャを、
こんな風に処理ごとにモジュール分割しているのも地味ですが重要なポイントです。
モジュールは名称によって自動で並び変わってしまいますので、
頭文字+数字を付けて並びをコントロールしましょう。
汎用関数の作成(年度版のDateSirial ほか)
【挑戦ポイント】
Get年度(日付 As Date) を「Functionプロシージャ」で作成してみてください。
こちらはまずその関数を作成しました。
' 年度の取得 Function Get年度(ByVal 日付 As Date) As Long If Month(日付) >= 4 Then Get年度 = Year(日付) Else Get年度 = Year(日付) - 1 End If End Function
こんな単純な関数でも、メインコードをこれだけスッキリかけるようになります。
Dim 注文書年度 As Long: 注文書年度 = Year(ws注文書.Range("C7")) If Month(ws注文書.Range("C7")) <= 3 Then 注文書年度 = 注文書年度 - 1 If WS販売データ.Range("F2") = 注文書年度 Then ' ⇩ 関数を使って書き換え If WS販売データ.Range("F2") <> Get年度(ws注文書.Range("C7")) Then
Functionと聞くと最初は難しそうに感じるかもしれませんが、
こういった単純な処理を切り出すだけなら案外簡単に作ることができます。
それでいて効果は抜群ですから、マクロづくりに慣れてきたら、
自作の汎用関数(ライブラリ)作りにぜひ挑戦してみましょう。
今回は汎用関数の例として、年度を取得する指定のDateSerialのほかに、
「Get最終行」「Isシートが存在する」「Isブックを開いている」
「ブックを開いていなければ開く」も作成してみました。
「Get最終行」はおなじみのEndプロパティによる最終行取得ですが、
頻出なだけに、これも関数にしておくとサクサクコードが書けるようになります。
他3つの関数は、簡単だけど分岐・ループが必要な処理たちです。
この辺の処理は簡単なんですが意外とメインコードの行数を食ってしまうので、
汎用関数にすることでメインコードを相当スッキリさせることができます。
◇ 汎用関数未使用のマクロ①解答
On Error Resume Next ' ファイルを開いていなければ開く Dim wb取込ファイル As Workbook Set wb取込ファイル = Workbooks(取込ファイル名) ' ← 開いていなければエラーでスキップ If wb取込ファイル Is Nothing Then ' ↑がスキップされていればファイルを開く Set wb取込ファイル = Workbooks.Open(path取込ファイル) End If If wb取込ファイル Is Nothing Then MsgBox "何らかのエラーでファイルが開けませんでした。": Exit Sub ' シートの存在チェック Dim ws注文書 As Worksheet: Set ws注文書 = wb取込ファイル.Worksheets("注文書") If ws注文書 Is Nothing Then MsgBox "注文書シートがないファイルです。": Exit Sub On Error GoTo 0
◇ 汎用関数を使用した今回のマクロ①解答
' 対象ブックを開く Dim path保存フォルダ As String: path保存フォルダ = Replace(path取込ファイル, "\" & 取込ファイル名, "") Dim wb取込ファイル As Workbook Set wb取込ファイル = ブックを開いていなければ開く(path保存フォルダ, 取込ファイル名) ' シートの存在チェック If Isシートが存在する("注文書", wb取込ファイル) = False Then MsgBox "注文書シートがないファイルです。": Exit Sub Dim ws注文書 As Worksheet: Set ws注文書 = wb取込ファイル.Worksheets("注文書")
全体が見渡しやすくなり、各行もかなり読みやすくなっていますね。
このようによく書くコードを汎用関数にしておくと、
メインコードを劇的に書きやすく&読みやすくすることができます。
単純な処理をFunctionにするだけなら思ったよりも簡単ですので、
是非とも挑戦してみてください。
詳しくはこちらの記事をどうぞ。
www.limecode.jp
注文書ファイルのチェック
【挑戦ポイント】
注文書ファイルでないものを取り込まないよう実行前にチェックしてみてください。
同日同購入者の注文書はないものとしますので、それをもって「同じ注文書を2度取り込まない」チェックを追加してください。
これらのチェックを行い、除外したファイルもログに記録してください。
こちらはそれぞれの判定をFunction
「Is注文書シート」「Is指定注文書が販売データに取込済」
を作って対応しました。
Function Is注文書シート(判定シート As Worksheet) As Boolean Is注文書シート = 判定シート.Range("B2") = "注 文 書" _ And 判定シート.Range("D24") = "合計(税込)" End Function Function Is指定注文書が販売データに取込済(ws注文書 As Worksheet) As Boolean ' そもそも年度が異なればExit If WS販売データ.Range(Adrs販売データ_年度) <> Get年度(ws注文書.Range(Adrs注文書_注文日)) Then Exit Function ' 判定キーの取得 Dim key月 As Long: key月 = Month(ws注文書.Range(Adrs注文書_注文日)) Dim key日 As Long: key日 = Day(ws注文書.Range(Adrs注文書_注文日)) Dim key購入者 As String: key購入者 = ws注文書.Range(Adrs注文書_購入者) ' 全キーが一致するデータがあるか判定 Dim R As Long For R = R1st販売データ To Get最終行(WS販売データ) If WS販売データ.Cells(R, CNo販売データ.月) = key月 _ And WS販売データ.Cells(R, CNo販売データ.日) = key日 _ And WS販売データ.Cells(R, CNo販売データ.購入者) = key購入者 Then Is指定注文書が販売データに取込済 = True Exit Function End If Next End Function
こういった判定は何度も出てくるため関数化しておくと便利ですし、
なによりメインコードが
' 注文書のレイアウトが不一致 ElseIf Is注文書シート(ws注文書) = False Then WS実行シート.Cells(R_ログ, CNo取込ログ.結果) = "シートレイアウト不一致" ' 取込済 ElseIf Is指定注文書が販売データに取込済(ws注文書) Then WS実行シート.Cells(R_ログ, CNo取込ログ.結果) = "取込済の注文書"
これだけ簡潔に読みやすくなっているのがポイントです。
Boolean型を返す判定関数を「Is○○」という名称にしておくと、
「If Is○○ Then」と文章っぽく読めるようになっていいですね。
前述の通り、Sub/Function分割の真の目的は
「処理に見出しをつけて1行に要約する」ことです。
是非とも活用し、メインコードをスッキリ読みやすいコードにしてください。
以上で挑戦ポイントを網羅した完成版の解説を終わります。
マクロ①②の解答コードと見比べると、
全く同じ処理でも全然違う書き方になっているのがわかります。
ある程度処理が書けるようになったら、
メンテナンス性や可読性を意識したコードに挑戦してみましょう。
マクロの改修が楽になったり、バグの発生率を落とすことができます。
コードの書き方にはいろいろな方法がありますので、
いろいろなコードを眺めて参考にしてみてください。