Excel VBA 第1回 例外処理の対応（どのモジュールの、どの関数で例外は発生したのか？）

#はじめに
仕事でよくExcelを使います。その中でExcel VBAはとても便利です。
Excelさえインストールされていれば利用することができて、かなり作業を効率化することができます。
新たなソフトウェアのインストールが不要というところは非常に大きく、仕事で使用する場合、ソフトウェアのインストールに対し、会社に申請が必要だったりしますので、本当に助かります。

今までいろいろなマクロをVBAを使って作成してきて、ネットをみて自分なりのソースを作ってきました。今までまとめることがなかったので、備忘としてここに記載します。

関数の説明なんかは色々なサイトに記載がありますし今更VBA感はありますが、どちらかというと、使い勝手のよい誰でも助かるようなVBAとか関数などを載せていきたいと思います。
基本的に備忘であることから、自分がわかる程度で記載します。
もし理解できない内容があれば恐縮です。Qiitaに載せる以上、何かコメントいただきしたら改善は考えたいと思います。

#はじめに2
例外処理です。
例外処理なので、有用なVBAというわけではなく、どんなマクロにでも適用できる基本的な内容です。
以前マクロを作っていた時にあるサイトで見つけまして、それ以降、自分が作るマクロには必ずといっていいほど実装している内容です。

#今回実施する内容
VBAがプログラムである以上、プログラマーの意図しない動作は入り込んでしまうものです。
そんなとき、例外処理でそれを取得して処理をしていくわけです。
基本的に、極力例外処理は発生しないようにプログラミングしますので、例外処理が発生する場合は、だいたいそのエラーを表示させて、マクロを終了させることになります。（もちろん処理を続けるケースもあります）
そういったエラーを表示させて終了させる場合、発生したErrを取得して、それを画面で表示するだけでは、**どのモジュールの、どの関数**で例外処理が発生したかわかりません。

そこで、今回は、**どのモジュールの、どの関数で、どういったエラーが発生したのかを表示させる**方法を紹介します。
これは、ほとんど参考にした内容そのままなので、参考を見ていただいてもよいと思います。
まずは、動作のイメージです。具体的な内容はまた後で説明します。
![概要イメージ.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/405480/a40cdd66-3d36-4f0f-aa43-59efa2ac40c6.jpeg)

①エラークラスの初期化
本実装においては、オリジナルのエラークラスを作成します。そのクラスの初期化を行います。

②何かエラー発生
On Error GoTo内で何かエラーが発生した場合、「AnyErr」へ飛ばします。

③エラーモジュール名と関数名をセット
クラスの関数のSetErrorで、**どのモジュールの、どの関数のエラー**なのかを設定します。

④エラーメッセージの表示
③と例外で発生したエラーの内容を画面に表示します。

#ソースコード
[VBA_01_Exeption](https://github.com/Build-Mount/VBA_01_Exception)

#環境
OS:Windows 10 JP
Excel: Excel 2019 (32bit)

#参考
[中級者のためのExcel エクセルマクロVBA入門：On Error Goto を極める！](http://shuhho.hatenablog.com/entry/excelvba-102)

#用語
なし

#説明
今回使用するソースコードは以下のものがあります。

|コード|説明|
|:---|:---|
|Module1モジュール|VBAマクロの実態。|
|ErrModuleモジュール|Cls_Errorクラスの初期化のためのモジュール。|
|Cls_Errorクラス|エラークラス。|
|ErrFormフォーム|エラー表示用のフォーム。|

##エラークラスの導入 (Cls_Error.cls)
**どのモジュールの、どの関数**でエラーが発生したかを示すためにオリジナルのエラークラスを作成します。
まずは、ソースコードからです。その後でブロックごとに説明します。

```visualbasic:Cls_Error.cls
Option Explicit

'-----Constant-----
Private Const ERROR_NO As String = "ErrorNo"
Private Const ERROR_INFO As String = "ErrorInfo"
Private Const TOOL_VER As String = "ToolVer"
Private Const ERROR_PROJ As String = "ErrorProject"
Private Const ERROR_MODULE As String = "ErrorModule"
Private Const ERROR_PROC As String = "ErrorProc"
Private Const ERROR_MSG As String = "エラーが発生しました。" & vbLf & vbLf & _
                                    "エラー番号：          " & ERROR_NO & vbLf & _
                                    "エラー情報：          " & ERROR_INFO & vbLf & _
                                    "ツールver：           " & TOOL_VER & vbLf & _
                                    "発生モジュール名：  " & ERROR_MODULE & vbLf & _
                                    "発生関数名：        " & ERROR_PROC & vbLf & _
                                    "上記内容を、システム担当者にお知らせください。"

'-----Global variables-----
Private G_toolVersion As String
Private G_projectName As String
Private G_moduleName As String
Private G_procName As String
Private G_errorMsg As String

'クラスの初期化
Private Sub Class_Initialize()
    G_toolVersion = ""
    G_projectName = ""
    G_moduleName = ""
    G_procName = ""
    G_errorMsg = ""
End Sub

'-----Property-----
'Project名を取得するプロパティ。
'
'@return String プロジェクト名。
Public Property Get ProjectName() As String
    ProjectName = G_projectName
End Property

'Project名を設定するプロパティ。
'
'@ProjName String プロジェクト名。
Public Property Let ProjectName(ProjName As String)
    G_projectName = ProjName
End Property

'ツールバージョンを取得するプロパティ。
'
'@return String ツールバージョン。
Public Property Get ToolVersion() As String
    ToolVersion = G_toolVersion
End Property

'ツールバージョンを設定するプロパティ。
'
'@Version String ツールバージョン。
Public Property Let ToolVersion(Version As String)
    G_toolVersion = Version
End Property

'-----Function-----
'ツールのProject名とバージョンを設定する。
'
'@projName String プロジェクト名。
'@version String ツールのバージョン。
Public Sub SetProject(ProjName As String, Version As String)
    G_projectName = ProjName
    G_toolVersion = Version
End Sub

'エラーが発生したモジュール名と関数名を設定する。
'
'@ModuleName String モジュール名。
'@ProcedureName String 関数名。
Public Sub SetError(ByVal ModuleName As String, ByVal ProcedureName As String)
    If G_moduleName = "" Then
        G_moduleName = ModuleName
        G_procName = ProcedureName
    End If
End Sub

'エラーを表示する。
Public Sub ShowErrMsg()
    Dim ErrMsg As String
    
    ErrMsg = ERROR_MSG
    ErrMsg = Replace(ErrMsg, ERROR_NO, Err.Number)
    ErrMsg = Replace(ErrMsg, ERROR_INFO, Err.Description)
    ErrMsg = Replace(ErrMsg, ERROR_PROJ, G_projectName)
    ErrMsg = Replace(ErrMsg, TOOL_VER, G_toolVersion)
    ErrMsg = Replace(ErrMsg, ERROR_MODULE, G_moduleName)
    ErrMsg = Replace(ErrMsg, ERROR_PROC, G_procName)
    
    Call ErrForm.ShowErrMsg(ErrMsg)
End Sub

```

###Constant部分
ERROR_NO, ERROR_INFO, TOOL_VER, ERROR_PROJ, ERROR_MODULE, ERROR_PROCについては、すべてエラーメッセージERROR_MSGを表示にあたって、適切な値に変換するするために定義しています。
別にERROR_MSGに直接書いておいても問題はありません。
ERROR_MSGは、画面に表示する文字列であり、フォーマットは自由です。

###Global variables部分
例外処理が発生したときに表示させる内容を本クラスで使用するために、定義します。
G_toolVersionは、このVBAのバージョン
G_projectNameは、このVBAのプロジェクト名
G_moduleNameは、このVBAのモジュール名
G_procNameは、例外処理が発生した関数名
G_errorMsgは、実際に表示するメッセージ
です。
Constantで定義したものをベースに、書き換えを行って、Global variablesに保存するという流れになります。

###クラスの初期化
Cls_Errorが初期化されたときにGlobal variablesの値を""で初期化します。

###Property
プロジェクト名を取得するGetとプロジェクト名を設定するLetやツールバージョンを取得するGetとツールバージョンを設定するLetを定義します。

###Function
####SetProject
例外処理が発生したときに、そのモジュール名、関数名を設定します。
If文で`G_moduleName= ""`としています。
これは、後程出てきますが、すでにモジュール名をいったん設定済みの場合は上書きしないことを示します。

####ShowErrMsg
例外処理が発生したときに、設定された値を画面表示します。
Replaceを使って、ErrMsg内のConstantの値をGlobal variablesに保存された値に置き換えます。
最後に、ErrForm.ShowMsgで画面に表示します。
この部分は、MsgBoxを使ってもよいのですが、誰かがそのマクロを使って例外処理が発生したときに、そのエラー発生内容を連絡してもらうために、ErrFormを作成し、テキストボックスに表示させることで、コピー＆ペーストを容易にしています。

##ErrFormの作成 (ErrForm.frm)
画面表示するだけのため単にフォームを作成し、テキストボックスを作り、あとはクローズボタンだけです。

```visualbasic:ErrForm.frm
Option Explicit

Public Sub ShowErrMsg(ErrMsg As String)
    ErrorText = ErrMsg
    Me.Show
End Sub

Private Sub CommandButton1_Click()
    Unload Me
End Sub
```

ErrorTextがエラーメッセージを表示するためのTextBoxです。
今回のエラー表示では、複数行で表示するため、このTextBoxのプロパティで、MultiLineをTrueに設定しておく必要があります。
また、LockedをTrueにして、エラー内容を編集できないようにしておくこともよいと思います。

##ErrModuleの作成 (ErrModule.bas)

```visualbasic:ErrModule.bas
Option Explicit

Public G_clsErr As Cls_Error

'Cls_Errorクラスのインスタンスを作成する関数。
'プロジェクト名とバージョンを初期化する。
'
'@ProjName String プロジェクト名。
'@Version String ツールバージョン。
'@return Cls_Error Cls_Errクラスのインスタンス。
Public Function CreateErrClass(ProjName As String, Version As String) As Cls_Error
    Set CreateErrClass = New Cls_Error
    Call CreateErrClass.SetProject(ProjName, Version)
End Function
```
###G_clsErrの定義
Cls_errorクラスをG_clsErrとして定義します。

###CreateErrClassの定義
この程度ならばソースコードに直接記載してもよいレベルですが、CreateObject関数に倣って関数化しています。
ここで、初期化しつつ、プロジェクト名、ツールバージョンを設定します。

##実際に使う本体 (Module1.bas)
```visualbasic:Module1.bas
Option Explicit

Public Sub Test()
    Dim No As Integer
    
    Set G_clsErr = CreateErrClass("Project", "1.0")
    
    On Error GoTo AnyErr
    
    No = 1.23456789012346E+15
    Exit Sub
AnyErr:
    Call G_clsErr.SetError("Module1", "Test")
    Call G_clsErr.ShowErrMsg
End Sub
```

ここは、実際に関数にエラークラスを利用するだけです。
今回の例では、Integerに設定した値が範囲外で、エラー番号：6で、「オーバーフローしました。」というエラーが発生します。

###Test Sub関数
最初に
`Set G_clsErr = CreateErrClass("Project", "1.0")`
で、クラスの初期化と、プロジェクト名、ツールバージョンを設定します。

`On Error GoTo AnyErr`を記載して、
IntegerのNoにオーバーフローする値を設定します。
その後に、`Exit Sub`の記載は忘れないように。忘れると、常にAnyErrが動作してしまいます。

`AnyErr:`では、
`Call G_clsErr.SetErr("Module1", "Test")`
で、このモジュール名と関数名を設定します。
`Call G_clsErr.ShowErrMsg`
で、エラー内容を表示します。

##応用
上記で説明したのは、基本的な使い方です。
でも、これだと、他の関数でエラーが発生する場合どうするのか？ということになりますので、もうちょっと追加します。

```visualbasic:Module1.bas
Option Explicit

Public Sub Test()
    Dim No As Integer
    
    Set G_clsErr = CreateErrClass("Project", "1.0")
    
    On Error GoTo AnyErr
    
    No = GetValue
    Exit Sub
AnyErr:
    Call G_clsErr.SetError("Module1", "Test")
    Call G_clsErr.ShowErrMsg
End Sub


Private Function GetValue() As Integer
    Dim No As Integer
    
    On Error GoTo AnyErr
    
    No = 1.23456789012346E+15
    GetValue = No
    
    Exit Function
AnyErr:
    Call G_clsErr.SetError("Module1", "GetValue")
    Call Err.Raise(Err.Number, , Err.Description)
End Function
```

変わったところは、Noの値をGetValue関数にまかせたことです。

###GetValue
Test Sub関数と同様に、`On Error GoTo AnyErr`を記載して、`No`に範囲外の値を割り当てます。
当然例外処理が発生します。
`AnyErr:`では、
`Call G_clsErr.SetErr("Module1", "GetValue")`
とし、引数二つ目の関数名が変わりました。
`Call Err.Raise(Err.Number, , Err.Description)`
となります。
Err.Raiseは、エラーを発生させる関数です。
GetValueで発生したエラーはいったん例外処理が発生しましたが、ここでTest Sub関数に戻していきます。
そうすると、Test Sub関数でも`AnyErr`になります。
`G_clsErr.SetErr("Module1", "Test")`
の部分は、前述しましたが、If文でモジュール名が設定されている場合は処理されません。
`Call G_clsErr.ShowErrMsg`
でエラーが表示されます。

#おわりに
今回のソースは、本当に参考のソース通りでもうしわけありませんが、とても有用で、私がVBAを作る場合必ず実装するくらいのものなので、忘れないように載せました。
以上です。