見出し画像

マクロのある暮らし(7回目) - エラーに対処しつつブックを安全に利用する

こんにちは!なるーらぼです!今回は珍しく続き物です。
前回はこちら。

それではおさらいからいきましょう。

おさらい

前回は別のブックを2つ開いて、片方から片方へデータを移して別名保存する方法をご覧いただきました。最終的なコードを再掲しておきます。

Sub note_mu()
 Const soldname As String = "売上データ.xlsx"
 Const tempname As String = "領収証ひな形.xlsx"
 Const mydocuments As String = "\Documents\"
 Dim userdoc As String
 Dim soldbook As Workbook
 Dim tempbook As Workbook
 userdoc = Environ("UserProfile") & mydocuments
 Set soldbook = Workbooks.Open(userdoc & soldname)
 Set tempbook = Workbooks.Open(userdoc & tempname)
 '' 売上データのレイアウト
 '' A:領収番号、B:名前、C:金額、D:日付、E:発行済み
 Dim dataline As Variant
 dataline = soldbook.Worksheets(1).Range("A2:E2")
 With tempbook.Worksheets(1)
  .Range("E2").Value = dataline(1, 1)
  .Range("C4").Value = dataline(1, 2)
  .Range("C6").Value = dataline(1, 3)
  .Range("C11").Value = Format(dataline(1, 4), "yyyy年mm月dd日")
 End With
 tempbook.SaveCopyAs userdoc & dataline(1, 1) & "_" & dataline(1, 2) & ".xlsx"
 soldbook.Close False
 tempbook.Close False
 Set soldbook = Nothing
 Set tempbook = Nothing
End Sub

長いですね。では今回はこれを複数行データに対応できるようにするところから行きます。複数行に対応するためにはデータがある間は繰り返しを行ってひな形へのデータ写しと別名保存を行います。データがなくなったら処理を終了するようにします。

Sub note_mu()
 Const soldname As String = "売上データ.xlsx"
 Const tempname As String = "領収証ひな形.xlsx"
 Const mydocuments As String = "\Documents\"
 Dim userdoc As String
 Dim soldbook As Workbook
 Dim tempbook As Workbook
 userdoc = Environ("UserProfile") & mydocuments
 Set soldbook = Workbooks.Open(userdoc & soldname)
 Set tempbook = Workbooks.Open(userdoc & tempname)
 '' 売上データのレイアウト
 '' A:領収番号、B:名前、C:金額、D:日付、E:発行済み
 Const startcol As Integer = 1
 Const endcol As Integer = 5
 Const startrow As Integer = 2
 Dim dataline As Variant
 Dim soldsheet As Worksheet
 Dim row As Integer
 Dim cols As Variant
 '' 長いので短縮のためにオブジェクトをつかんでおく
 Set soldsheet = soldbook.Worksheets(1)
 '' 開始行
 row = startrow
 
 Do
  dataline = soldsheet.Range( _
        soldsheet.Cells(row, startcol), _
        soldsheet.Cells(row, endcol))
  With tempbook.Worksheets(1)
   .Range("E2").Value = dataline(1, 1)
   .Range("C4").Value = dataline(1, 2)
   .Range("C6").Value = dataline(1, 3)
   .Range("C11").Value = Format(dataline(1, 4), "yyyy年mm月dd日")
  End With
  tempbook.SaveCopyAs _
   userdoc & dataline(1, 1) & "_" & dataline(1, 2) & ".xlsx"
  row = row + 1 '' 次の行へ
 Loop Until WorksheetFunction.CountA( _
        soldsheet.Range( _
         soldsheet.Cells(row, startcol), _
         soldsheet.Cells(row, endcol))) = 0
 soldbook.Close False
 tempbook.Close False
 Set soldsheet = Nothing
 Set soldbook = Nothing
 Set tempbook = Nothing
End Sub

な、長い…ほんとうはサブルーチンに分割していったほうが読みやすさも向上していいのですが、バラバラにしてしまうと説明がややこしくなるので敢えてこのままいきたいと思います(キリッ
少し解説をしますと、Rangeメソッドに渡すセルの範囲を文字列の「A2:E2」という形式からCellsメソッドでFrom-Toにした形式に変更しています。
これで行数のカウンタを1ずつ足しこんでいくだけで範囲が変わっていきます。

さらに横着をしまして、データがなくなったことをワークシート関数の「CountA関数」でチェックしています。指定範囲の値が入ったセルの数をカウントしてくれるので、これがゼロを返すようになったら「データなし」とみなしてもいいでしょう、ということです。

エラーへの対処をしよう

こうしたマクロで無関係の別のファイルを操作することになると、問題になるのが「想定していない状況」によるエラーの発生です。

例えば「同じフォルダ内に保存していくとわけがわからなくなるので別のフォルダにしようぜ、例えば領収書発行、とか」といったような話が上がってくることはありますよね:)

そうしたときに保存のところを次のようにしたとします。

tempbook.SaveCopyAs _
 userdoc & "\領収書発行\" _
 & dataline(1, 1) & "_" & dataline(1, 2) & ".xlsx"

これで動かすと何が起きるでしょうか?

「ホッワーーーイッッッ!!!!!」ってなります。それにこのまま終了してください。ひな形も売り上げデータもブックが開いたままです…

エラーが発生したときも安全にブックだけは閉じておきたいですよね。

そうしたときにはVBAではエラーが発生したら制御を捕まえて特定の行へジャンプさせる機能を持っています。「On Error Goto ****」という書き方です。マクロに書いてあるのを見かけたことがあるでしょう。

これはエラーが発生したらコード内に書いたラベルへジャンプさせることができます。ただし、わかってくれるのは同じサブルーチン内のラベルです。ここで捕捉さえできれば、「Err」というエラー情報を含んだオブジェクトからエラー番号やエラーの内容をある程度(全部、とは言いません…)教えてもらうことができます。

なお、よくネット上の記事にあるのはサブルーチンの先頭に「On Error Goto」を書いてあるものがありますが、ラベルはわかるのであればいくつ書いてもいいですし、エラーの内容によってラベルを使い分けてもいいです。

先ほどのコードだと、次の箇所は保存に失敗するエラーが発生しやすそうですよね。いまは固定の場所になっていますが、もしも保存先のフォルダ名をセルなどのユーザー入力から受け取るようになったら発生確率は急激に上がります。

ではこの付近にだけエラーを捕まえるところを指定しておきます。

On Error GoTo SaveFile_Failure
tempbook.SaveCopyAs _
 userdoc & "\領収書発行\" _
 & dataline(1, 1) & "_" & dataline(1, 2) & ".xlsx"
On Error GoTo 0

ラベルはサブルーチンの終わり付近に設置して、「SaveFile_Failure」なんかにしておきますか。

SaveFile_Failure:
 If Err.Number <> 0 Then
  MsgBox "ブックが保存できませんでした。" & vbCrLf _
      & "(" & Err.Number & ")" _
      & Err.Description, vbExclamation,
      ThisWorkbook.Name
 End If
soldbook.Close False
tempbook.Close False
Set soldsheet = Nothing
Set soldbook = Nothing
Set tempbook = Nothing

これで動作させるとメッセージが表示されますね。そしてブックはすべて閉じられます。

ブックが開いているときなどもラベルをつけてジャンプさせてもいいですよ。ジャンプさせるには悪名高い「Goto」文を使います。できれば下方向への移動にのみ使うことを強くお勧めします。

ブックを開く前のエラーにも対処しておこう

現段階ではブックを開く前にエラーが発生しても対処することができません。だって、ブック保存のところにしかトラップを仕掛けていませんから。ですから、もしちょっと手が当たって「売上」の「上」という字を削除してしまったことに気づかず実行するとこのようになります。

ではエラーを捕まえる部分を少し修正してみます。

OpenFile_Failure:
 If Err.Number <> 0 Then
  MsgBox "ブックを開くことができませんでした。" & vbCrLf _
  & "(" & Err.Number & ")" & Err.Description, _
  vbExclamation, ThisWorkbook.Name
 End If

SaveFile_Failure:

さきほどのラベルより上に持ってきました。これで実行してみるとメッセージボックスが表示されますが…

あれ!?保存できなかったっていうエラーも表示された…それはそうですね。だって続けて「SaveFile_Failure」ラベルがありますし、その中ではエラーになったときに表示するメッセージも用意されています。それに、エラー発生を番号がゼロ以外であることで判断していますので、もちろんエラーはあります!!ですから2回表示されて当然です。しかもそれもOKをクリックするとさらに意味のわからないエラーが…では「SaveFile_Failure」ラベルのブロックより下にさらに「Normal_End」というラベルを用意して、そこへジャンプさせるようにしましょう。

OpenFile_Failure:
 If Err.Number <> 0 Then
  MsgBox "ブックを開くことができませんでした。" & vbCrLf _
  & "(" & Err.Number & ")" & Err.Description, _
  vbExclamation, ThisWorkbook.Name
 End If
 GoTo Normal_End

SaveFile_Failure:
 If Err.Number <> 0 Then
  MsgBox "ブックが保存できませんでした。" & vbCrLf _
  & "(" & Err.Number & ")" & Err.Description, _
  vbExclamation, ThisWorkbook.Name
 End If

Normal_End:

よし、これでOK!と思って実行してみるのですが、やはり意味のわからないエラーが表示されますよね…

このエラーの正体はなんなのでしょう?では「デバッグ」のほうをクリックしてみてください。答えがわかります。

ブックを閉じるところ・・・?もう、お分かりでしょうか。
そうです、まだ開いていないブックを閉じようとしているのでエラーが発生しているのです。原因が分かってしまえば対処は簡単です。「使わない」のところへラベルを移動させて、「Normal_End」ラベルがあった場所には別の「Close_Books」ラベルを配置します。これで、とりあえず開いたブックを閉じたくなったらここへジャンプさせればいいのですから。

Close_Books:
 soldbook.Close False
 tempbook.Close False

Normal_End:
 Set soldsheet = Nothing
 Set soldbook = Nothing
 Set tempbook = Nothing

これで不用意に閉じられなくなるのでエラーは発生しなくなります。

最後に

今回は前回のコードを引き続きでエラーへの対処についてみてきました。
実際にはサブルーチンに分割してひとつひとつのサブルーチンで行うことをできるだけ小さくするとエラーへの対処もやりやすくなりますし、エラーの原因特定も早くすることができます。

そうしたサブルーチンは値を真偽値で返すようにするとサブルーチンを利用しているコードから「うまくいったのか」わかりやすくなります。エラーが起きたら偽(False)を返すようにするとIf文で制御しやすくなりますので。

ではまた。

この記事が気に入ったらサポートをしてみませんか?