和風スパゲティのレシピ

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

VBA問題#1「領収書PDFを出力」マクロ②解答

ExcelVBA練習問題シリーズ#1「領収書PDFを出力」マクロ②解答コードです。

出力元データ
出力帳票

もちろんこれが正解という訳ではなく、いろいろなやり方あると思いますが、
ひとつの解答としてご参考ください。

※ 挑戦ポイントには挑戦していない基本コードになります。


◇ 出題ページはこちら
www.limecode.jp
◇ 解答ページはこちら
VBA問題#1「領収書PDFを出力」マクロ①解答
VBA問題#1「領収書PDFを出力」マクロ②解答【本ページ】
VBA問題#1「領収書PDFを出力」解答完成版(挑戦ポイント制覇)
 

ソースコード

Option Explicit

' マクロ② 領収書を一括出力
Sub 一括出力指定の全データを領収書PDFに出力する()

    Dim LastR販売データ As Long
    LastR販売データ = WS販売データ.AutoFilter.Range.Rows.Count + WS販売データ.AutoFilter.Range.Row - 1
    
    ' 実行条件チェック
    If WorksheetFunction.CountIf(WS販売データ.Range(WS販売データ.Cells(5, 11) _
                                                                          , WS販売データ.Cells(LastR販売データ, 11)), 1) = 0 Then
        MsgBox "一括出力列に「1」が立っている行がありません。"
        Exit Sub
    End If
    
    ' 出力フォルダのチェックと作成
    Dim Path出力フォルダ As String: Path出力フォルダ = ThisWorkbook.Path & "\領収書"
    If Dir(Path出力フォルダ, vbDirectory) = "" Then MkDir Path出力フォルダ

    ' 一括出力列の「1」以外の値をクリア
    Dim R As Long
    For R = 5 To LastR販売データ
        If WS販売データ.Cells(R, 11) <> 1 _
        And WS販売データ.Cells(R, 11) <> "" Then
            WS販売データ.Cells(R, 11) = ""
        End If
    Next

    ' データシート全行をループ
    For R = 5 To LastR販売データ
        
        ' 一括出力が「1」でかつ出力済でない行を処理
        If WS販売データ.Cells(R, 11) = 1 _
        And WS販売データ.Cells(R, 10) = "" Then
        
            ' 領収書への印字
            WS領収書.Range("D3") = WS販売データ.Cells(R, 2) 'No
            WS領収書.Range("C7") = WS販売データ.Cells(R, 3) ' 購入者
            WS領収書.Range("F9") = WS販売データ.Cells(R, 9) ' 売上
            WS領収書.Range("F11") = WS販売データ.Cells(R, 6) ' 品物
            
            ' 購入日
            Dim y As Long, m As Long, d As Long
            m = WS販売データ.Cells(R, 4)
            d = WS販売データ.Cells(R, 5)
            y = WS販売データ.Range("F2") + IIf(m <= 3, 1, 0)
            WS領収書.Range("G3") = DateSerial(y, m, d)
            
            ' ファイル名を設定
            Dim 出力ファイル名 As String
            出力ファイル名 = "領収書" & Format(DateSerial(y, m, d), "yyyymmdd") _
                                             & "(" & WS領収書.Range("C7") & ").pdf"
            
            ' PDFに出力
            WS領収書.ExportAsFixedFormat xlTypePDF, Path出力フォルダ & "\" & 出力ファイル名
        
            ' 出力日の印字
            WS販売データ.Cells(R, 10) = Format(Date, "m/d") & "済"
            WS販売データ.Cells(R, 11) = "完了"
        
        ' 一括出力が「1」だが出力済の行
        ElseIf WS販売データ.Cells(R, 11) = 1 _
        And WS販売データ.Cells(R, 10) <> "" Then
        
            WS販売データ.Cells(R, 11) = "失敗"
        
        End If
        
    Next ' データシート全行をループ
    
    MsgBox "領収書PDFの一括出力を完了しました。"

End Sub

' 一括出力列のクリア
Sub 一括出力列をクリアする()

    Dim LastR販売データ As Long
    LastR販売データ = WS販売データ.AutoFilter.Range.Rows.Count + WS販売データ.AutoFilter.Range.Row - 1
    
    WS販売データ.Range(WS販売データ.Cells(5, 11) _
                              , WS販売データ.Cells(LastR販売データ, 11)).Value = ""

End Sub

解説

各コードの解説と工夫しているポイントは以下の通りです。

全体を通して

Forループ文の中にある「領収書に印字してPDFに出力」する部分は、
マクロ①と同じコードを使用しています。

こちらについての解説はマクロ①の解答ページをご参考ください。

www.limecode.jp


今回のコードはマクロ①をFor文で回しているコードのため、
メインコードの解説は上記の記事を見てもらえればと思います。


本記事ではFor文の組み方、If文の組み方のみ解説します。

For文のカウンタは意味のある単語を使いましょう

今回はデータ全行をループするFor文を書きました。

まず重要なのが、「行だからRを変数名とした」点です。

Dim R As Long
For R = 5 To LastR販売データ

 
プログラミングの文化的なお作法として、
ループのカウンタに「i」を使っているコードをよく見かけます。


しかしカウンタに「i」を使ってしまうと、

  • シート番号と行番号にどちらも「i」を使うことができない。
  • すでに「i」を使っているコード同士をコピペ合体できない。
  • 次のカウンタを「j」にすると「i」と似ていて読みづらい。

などの問題があります。


実体のない配列を扱うプログラミング言語と違い、
ExcelVBAは「シート」「行」「列」など実物が存在するオブジェクトを扱います。

それらのカウンタは「シートNo」「R」「C」を用いておけば、
コードも読みやすく、コピペ合体などもしやすくなりますので採用してみてください。

www.limecode.jp

一括出力列のクリアを単独のFor文に

今回のコードは「一括出力列が1の行を領収書に出力」するのがメインですが、
ついでに「前回のログ(完了/失敗)をクリアする処理」が必要です。


この処理はメインコードと同じループでやっても良いのですが、
本コードでは単独のFor文をもう一つ作って対応しました。

' 一括出力列の「1」以外の値をクリア
Dim R As Long
For R = 5 To LastR販売データ
    If WS販売データ.Cells(R, 11) <> 1 _
    And WS販売データ.Cells(R, 11) <> "" Then
        WS販売データ.Cells(R, 11) = ""
    End If
Next

 
こういった大したことがない処理をメインループから外すことで、
メインループをシンプルな構造に保つことができます。


一見For文を2回まわして二度手間に感じてしまいますが、
For文自体はRを足し算しているだけ」ですので、
実は速度面でも全く影響はありません。


処理ごとにFor文を分けることで複数の処理を同時に考える必要もなくなり、
さらにはテストも別々に行うこともできます。

「ループは一本でやらなければいけない」という思い込みがあったかたは、
是非一度この手法も試してみて下さい。


www.limecode.jp

出力済の行を判定するIF文

こちらはIf文の書き方の話です。

今回の処理は「出力列が1の行が対象」で、
その中で「未出力なら[実行] / 出力済なら[失敗]を印字」という分岐があります。


この分岐を表現するとき、分岐をモレなくダブりなくやるなら、
↓のようなコードになります。

If WS販売データ.Cells(R, 11) = 1 Then
    If WS販売データ.Cells(R, 10) = "" Then

    ' メインロジック
    
    Else

        WS販売データ.Cells(R, 11) = "失敗"

    End If
End If

 
しかし見てわかる通り、今回のコードは↓のように組みました。

If WS販売データ.Cells(R, 11) = 1 _
And WS販売データ.Cells(R, 10) = "" Then

    ' メインロジック

ElseIf WS販売データ.Cells(R, 11) = 1 _
And WS販売データ.Cells(R, 10) <> "" Then

    WS販売データ.Cells(R, 11) = "失敗"

End If

 
わざわざ「一括出力が1」の判定も2回やっていますし、
「出力列が空かどうか」もElseを使わずもう一度判定しています。


こちらも一見無駄が多いように見えますが、もちろんこうしている理由があります。

まず一つ目の理由は「メインロジックのインデントを減らせる」こと。

もうひとつは「上方にあるIfを見に行かないとElseの意味がわからない」対策です。


実際のコードではメインロジックは数十行~数百行にもわたるため、
Elseとだけ書いてあってもなんだかわからないことが多々あります。

    ' ↑ 手掛かりははるか上方

    Else ' ← なんの「それ以外」かわからない

        WS販売データ.Cells(R, 11) = "失敗"

    End If
End If ' ← なにが二重の判定だったかわからない

 
今回のコードはそこそこ単純な分岐だったので、
この書き方でなければいけないというほどではありませんでした。

しかし、複雑な分岐を「モレなくダブりなく」と考えてしまうと、
ただでさえ複雑なロジックをより複雑にし、読みのも大変にしてしまいます。


If文は冗長に判定しても速度にはほぼ影響はありませんので、
マクロによっては読みやすさを重視して書いてみてください。

www.limecode.jp




以上でマクロ②の解説を終わります。

基本コードの詰め合わせでしたが、
読みやすく書こうとすると、工夫できるポイントがたくさんありましたね。


マクロ②も作成できた方は、
是非挑戦ポイントを盛り込んだ完成版に着手してみてください。