AccessVBAでレポートをPDF出力するときの落とし穴5つと対策

抽出条件を付けてPDF出力したのに全件が出てしまう落とし穴のイメージ AccessVBA

AccessVBAでレポートをPDF出力したら、関係ないデータまで出てきた。

Accessで見積書や請求書をPDFにする処理は、DoCmd.OutputTo を使えば数行で書けます。ところが実際に使っていると、次のようなことが起きます。

– 1件分だけ出したいのに、全件分のページが出力された

– 普段は正しく出るのに、ときどき別のデータが混ざる

– データが0件のときに、中身が空のPDFができる

– お客様名に「/」が入っているとエラーで止まる

– PDFを開いたまま、もう一度出力すると「既に存在します」と言われる

シチュエーション

1件ずつPDFにする帳票
見積書・請求書・納品書など、選んだデータだけを出力する

ファイル名にお客様名や物件名を入れている
「〇〇邸_見積書.pdf」のように自動で名前を付ける

出力したPDFをすぐ開いて確認する
確認して直して、もう一度出力する

この記事では、検証用のデータベースで実際に起きることを確認しながら、5つの落とし穴と対策を紹介します。

落とし穴1:OutputToだけでは全件出力される

DoCmd.OutputTo には、抽出条件を指定する引数がありません。

DoCmd.OutputTo acOutputReport, "R_受注明細", acFormatPDF, "C:\work\受注明細.pdf"

このように書くと、レポートのレコードソースの全件がPDFになります。

受注番号=2の2行だけを出したいのに、OutputToだけでは全6行のPDFが出力される

対策
先に DoCmd.OpenReport で抽出条件を付けて非表示で開き、開いているレポートを OutputTo で出力します。

DoCmd.OpenReport "R_受注明細", acViewPreview, , "受注番号=2", acHidden
DoCmd.OutputTo acOutputReport, "R_受注明細", acFormatPDF, "C:\work\受注明細.pdf"
DoCmd.Close acReport, "R_受注明細", acSaveNo

OutputTo は、同じ名前のレポートが開いていれば、その開いているレポート(抽出条件が付いた状態)を出力します。

落とし穴2:レポートが開いたままだと抽出条件が効かない

落とし穴1の対策で「ときどき別のデータが混ざる」原因がこれです。同じレポートが既に開いていると、OpenReport の抽出条件が反映されません。

検証用のデータベースで、レポートを条件なしで開いた状態から、「受注番号=2」で OpenReport → OutputTo を実行したところ、受注番号2の2行ではなく全6行がPDFに出力されました。

レポートが開いたままだと、受注番号=2で開き直しても全6行のPDFが出力される

レポートが開いたままになるのは、次のような場合です。

– 開発中にレポートをデザインビューや印刷プレビューで開いたまま

– 前回の出力処理がエラーで途中終了し、非表示のレポートが残った

対策
OpenReport の前に、開いていれば閉じます。

If CurrentProject.AllReports("R_受注明細").IsLoaded Then
    DoCmd.Close acReport, "R_受注明細", acSaveNo
End If

落とし穴3:データが0件のとき

抽出条件に合うデータが0件でも、エラーにはならず、そのまま出力されます。検証用のデータベースでは、見出しと空の枠だけのPDFができました。

受注番号=99(0件)で出力すると、見出しと空の枠だけのPDFができる

レポートの「空データ時」イベントで Cancel = True にしておくと、中身が空のPDFは出なくなります。

Private Sub Report_NoData(Cancel As Integer)
    Cancel = True
End Sub

ただしこの場合、OpenReport がエラー2501「OpenReport アクションの実行は取り消されました。」になります。エラー処理で2501を受け取り、「データがありません」と案内するようにします。

対策
出力前に DCount で件数を確認し、0件なら出力しないのが分かりやすい方法です。レポートの空データ時イベントと両方入れておくと確実です。

落とし穴4:ファイル名に使えない文字

ファイル名にお客様名や物件名を入れていると、「A/B邸」のようにファイル名に使えない文字が入ることがあります。

検証用のデータベースでファイル名に「/」や「:」を入れて出力したところ、OutputTo がエラー2302「出力データを指定したファイルに保存できません。」になりました。

ファイル名に「/」が入っていると、エラー2302で保存できない

対策
ファイル名を作るときに、使えない文字(\ / : * ? " < > |)を置き換えます。後述の gf_SAFE_FILENAME を使います。

落とし穴5:出力先のPDFが開かれている

同じファイル名で出力し直すとき、前回のPDFがほかのアプリで開かれたままだと上書きできません。

検証用のデータベースで、出力先のファイルをほかのアプリから開いて書き込めない状態にして出力したところ、エラー2308「ファイル ‘〇〇.pdf’ は既に存在します。」になりました。実際の原因は「ファイルが開かれていて書き込めない」なのに、メッセージは「既に存在します」なので、利用者には何が悪いのか分かりません。

前回のPDFを開いたまま同じ名前で出力すると、エラー2308「既に存在します」になる

対策
出力前に同じ名前のファイルを Kill で削除し、それでも残っていれば「ファイルを閉じてください」と案内して中止します。ファイル名に日時(Format(Now, "yyyymmdd_hhnnss"))を入れて、毎回別の名前にしておく方法もあります。

まとめて対策したプロシージャサンプル

5つの対策をまとめた共通関数です。標準モジュールに以下のコードを貼り付けます。

'=========================================================
'  プロシージャ名:gf_OUTPUT_PDF
'  機能  :レポートを条件付きでPDFに出力します。
'
'  【引数】
'    strReport   :レポート名
'    strWhere    :抽出条件(例:"受注番号=2")
'    strFilePath :出力先のフルパス
'    strSource   :0件チェックに使うテーブル名/クエリ名(省略可)
'
'  【戻り値】
'    True :出力成功 / False :出力しなかった・失敗
'
'  【使用例】
'    Call gf_OUTPUT_PDF("R_受注明細", "受注番号=2", _
'                       "C:\work\受注明細_2.pdf", "T_受注明細")
'=========================================================
Public Function gf_OUTPUT_PDF(strReport As String, strWhere As String, _
                              strFilePath As String, _
                              Optional strSource As String = "") As Boolean
    On Error GoTo err_OUTPUT_PDF
    gf_OUTPUT_PDF = False
    '=== 0件なら出力しない ===
    If strSource <> "" Then
        If DCount("*", strSource, strWhere) = 0 Then
            MsgBox "出力するデータがありません。", vbExclamation
            Exit Function
        End If
    End If
    '=== 同名ファイルがあれば削除(開かれていて消せなければ中止)===
    If Dir(strFilePath) <> "" Then
        On Error Resume Next
        Kill strFilePath
        On Error GoTo err_OUTPUT_PDF
        If Dir(strFilePath) <> "" Then
            MsgBox "出力先のファイルが他のアプリで開かれています。" & vbCrLf & _
                   "ファイルを閉じてから、もう一度実行してください。" & vbCrLf & _
                   strFilePath, vbExclamation
            Exit Function
        End If
    End If
    DoCmd.Hourglass True
    '=== 既に開いているとWhere条件が効かないため、先に閉じる ===
    If CurrentProject.AllReports(strReport).IsLoaded Then
        DoCmd.Close acReport, strReport, acSaveNo
    End If
    '=== 条件を付けて非表示で開き、開いたレポートをPDFにする ===
    DoCmd.OpenReport strReport, acViewPreview, , strWhere, acHidden
    DoCmd.OutputTo acOutputReport, strReport, acFormatPDF, strFilePath
    DoCmd.Close acReport, strReport, acSaveNo
    DoCmd.Hourglass False
    gf_OUTPUT_PDF = True
    Exit Function
err_OUTPUT_PDF:
    DoCmd.Hourglass False
    If Err.Number = 2501 Then
        '=== レポートの「空データ時」でキャンセルされた ===
        MsgBox "出力するデータがありません。", vbExclamation
    Else
        MsgBox "【gf_OUTPUT_PDF】PDF出力中にエラー発生" & vbCrLf & _
               "番号:" & Err.Number & vbCrLf & _
               "内容:" & Err.Description, vbCritical
    End If
    On Error Resume Next
    DoCmd.Close acReport, strReport, acSaveNo
End Function

'=========================================================
'  プロシージャ名:gf_SAFE_FILENAME
'  機能  :ファイル名に使えない文字を「_」に置き換えます。
'
'  【使用例】
'    gf_SAFE_FILENAME("A/B邸 見積") → "A_B邸 見積"
'=========================================================
Public Function gf_SAFE_FILENAME(strName As String) As String
    Dim l_ng As Variant           ' 使えない文字
    Dim l_str As String           ' 変換後の文字列
    Dim i As Integer
    l_ng = Array("\", "/", ":", "*", "?", """", "<", ">", "|", vbTab, vbCr, vbLf)
    l_str = strName
    For i = 0 To UBound(l_ng)
        l_str = Replace(l_str, l_ng(i), "_")
    Next i
    gf_SAFE_FILENAME = Trim(l_str)
End Function

砂時計の表示について
DoCmd.Hourglass True は、エラーで抜けたときに戻し忘れると砂時計が出たままになります。エラー処理の先頭で必ず DoCmd.Hourglass False にしています。

実行方法

フォームのボタンのクリック時に、次のように書きます。

Private Sub Cmd_Pdf_Click()
    Dim l_path As String          ' 出力先のフルパス
    l_path = CurrentProject.Path & "\" & _
             gf_SAFE_FILENAME(Me.Txt_Tokuisaki & "_受注明細_" & Me.Txt_JuchuNo) & _
             "_" & Format(Now, "yyyymmdd_hhnnss") & ".pdf"
    If gf_OUTPUT_PDF("R_受注明細", "受注番号=" & Me.Txt_JuchuNo, l_path, "T_受注明細") Then
        Application.FollowHyperlink l_path
    End If
End Sub

出力に成功したら、FollowHyperlink でPDFを開いています。出力したファイルを自動で開く方法は、こちらの記事で紹介しています。

検証用のデータベースで、次の6つのケースを確認しました。

ケース結果
通常(受注番号=2)2行だけのPDFを出力
レポートが開いたままの状態で実行先に閉じるので、通常と同じ2行だけのPDFを出力
0件(strSourceを指定)「出力するデータがありません。」と表示して出力しない
0件(レポートの空データ時でキャンセル)エラー2501を受け取り、同じメッセージを表示
出力先のPDFが開かれている「他のアプリで開かれています」と表示して中止
ファイル名に「/」「:」「?」「_」に置き換えて出力

最後に

以上がAccessVBAでレポートをPDF出力するときの落とし穴と対策です。

特に落とし穴2(開いたままだと抽出条件が効かない)は、開発中にレポートを開いていたときだけ起きることが多く、原因に気付きにくい問題です。お客様の名前が入った帳票に別のお客様のデータが混ざると大きな問題になるので、出力処理は共通関数にまとめて、毎回「閉じてから開く」ようにしておくと安心です。

コメント

タイトルとURLをコピーしました