初心者のためのExcel(エクセル)マクロVBA入門-成績表マクロの作成:処理順番(フロー)の改善
さて、では答えです。前々回のMainのコードを見てみましょう。
Option Explicit Sub Main() '一般のエラーをキャッチする On Error GoTo cmnErr ' 必要な変数を作成する Dim shussekiNumber As String ' 出席番号を格納する変数 Dim name As String ' 氏名 Dim sansu As String ' 算数点数 Dim kokugo As String ' 国語点数 Dim rika As String ' 理科点数 Dim syakai As String ' 社会点数 Dim eigo As String ' 英語点数 Dim strErrMsg As String ' 入力チェックの結果を格納する変数 ' 出席番号を入力してもらう(ここで例外発生する!) shussekiNumber = InputBox("出席番号を入力してください", "出席番号入力", 1) ' InputCheckメソッドを呼び出してエラーメッセージを格納する strErrMsg = InputCheck(shussekiNumber) If strErrMsg <> "" Then 'エラーになった時の処理 MsgBox strErrMsg, vbOKOnly + vbExclamation, mdlDefine.ERROR_WINDOW_TITLE 'マクロを終了する Exit Sub End If ' 番号を使ってVlookUpで名前と点数を取得する(ここでも例外発生する!) name = Application.WorksheetFunction.VLookup(shussekiNumber, Sheet1.Range("A3:G13"), 2, False) '点数を取得して点数をチェックする sansu = Application.WorksheetFunction.VLookup(shussekiNumber, Sheet1.Range("A3:G13"), 3, False) strErrMsg = CheckScore(sansu) kokugo = Application.WorksheetFunction.VLookup(shussekiNumber, Sheet1.Range("A3:G13"), 4, False) strErrMsg = CheckScore(kokugo) rika = Application.WorksheetFunction.VLookup(shussekiNumber, Sheet1.Range("A3:G13"), 5, False) strErrMsg = CheckScore(rika) syakai = Application.WorksheetFunction.VLookup(shussekiNumber, Sheet1.Range("A3:G13"), 6, False) strErrMsg = CheckScore(syakai) eigo = Application.WorksheetFunction.VLookup(shussekiNumber, Sheet1.Range("A3:G13"), 7, False) strErrMsg = CheckScore(eigo) ' エラーだったらメッセージを表示して終了する If strErrMsg <> "" Then 'エラーになった時の処理 MsgBox strErrMsg, vbOKOnly + vbExclamation, mdlDefine.ERROR_WINDOW_TITLE 'マクロを終了する Exit Sub End If ' 所定の場所へ出力する Sheet1.Range("A19").Value = shussekiNumber Sheet1.Range("B19").Value = name Sheet1.Range("C19").Value = sansu Sheet1.Range("D19").Value = kokugo Sheet1.Range("E19").Value = rika Sheet1.Range("F19").Value = syakai Sheet1.Range("G19").Value = eigo 'マクロ終了 Exit Sub cmnErr: MsgBox "エラーが発生しました" & vbCrLf & _ "エラー番号:" & Err.Number & vbCrLf & _ "エラーの種類:" & Err.Description, vbOKOnly + vbExclamation, _ mdlDefine.ERROR_WINDOW_TITLE 'マクロを終了する Exit Sub End Sub
でした。これのどこに致命的なバグ(つまりエラー)がひそんでいるのでしょうか?
正解は、
一番最後のCheckScoreでエラーにならない限りはエラーチェックをスルーしてしまう
ということです。ちゃんとプログラムを追うとわかります。算数から英語までの点数を取得してその点数がきちんと数字かどうか?を順番にチェックしています。例えば社会に不正な点数(数字じゃない)が入っていたとします。すると・・・
- 算数の点数を取得する
- 数字かどうかチェックする
- 数字なので変数strErrMsgには空文字が入る
- 国語の点数を取得する
- 数字かどうかチェックする
- 数字なので変数strErrMsgには空文字が入る
- 理科の点数を取得する
- 数字かどうかチェックする
- 数字なので変数strErrMsgには空文字が入る
- 社会の点数を取得する
- 数字かどうかチェックする
- 数字でないので変数strErrMsgにはエラーメッセージが入る!
- 英語の点数を取得する
- 数字かどうかチェックする
- 数字なので変数strErrMsgには空文字が入る(あれ?)
- strErrMsgは空文字なので、エラーにはならない。。。。
わかりましたでしょうか?変数の性質は覚えていますか?もうだいぶ前で忘れてしまったかもしれませんが、既に値が入っている変数に違う値を入れると、最後に入れた値に書き換わりますこれが変数の大きな性質でした。このため、上のように書いてしまうと、最後にエラーでなければ結局、空の文字列となってしまい、正しくエラー処理ができないわけです。
まとめてチェックする
では、どうすればいいのでしょうか?解決策はたくさんありますが、一番簡単なのはstrErrMsgを毎回チェックすることです。つまり、エラーになった時点でプログラムを終了するようにしてしまいます。つまりどうするかというと、点数チェックのメソッドCheckScoreの中でエラーだったらもうマクロを終了するようにするのです。
つまり・・・
'''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' ' ' 点数チェックのメソッド ' CheckScore ' 引数:score As String ' 戻り値:String ' エラーの場合はプログラムを終了する ' エラーがない場合は空文字を返す。 ' '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''' Private Function CheckScore(ByVal score As String) As String '数字かどうかをチェックします。 If Not IsNumeric(score) Then ' 数字ではない場合「成績表の点数が不正です。処理を終了します。」 MsgBox mdlDefine.ERROR_MSG3, vbOKOnly + vbExclamation, mdlDefine.ERROR_WINDOW_TITLE 'マクロを終了する End End If ' それ以外はエラーなしとして空文字「""」を返す CheckScore = "" End Function
これで、mdlMainの処理に戻ることなくエラーの場合はその時点で終了します。そしてあとはMainの処理にあるIf strErrMsg <> "" ThenからEnd Ifまでを削除すれば、改善できたことになります。ばっちりです!
しかし・・・
本件、これで良し!とはしません。もちろん「動けば正解」なのでこれも間違いではないです。間違いではないですが、こんな風に書いてほしくないのです。マクロ(プログラム)とは思想です。どのように処理をしていき、見る人がわかりやすく綺麗に設計することがやはり大事なのです。
ちょっと、現在のプログラムの処理をざっくりと時系列で書いてみます。
- 出席番号を取得する
- 出席番号をチェックする
- エラーだったら終了
- 算数の点数を取得する
- エラーチェックする
- エラーだったら終了
- 国語の点数を取得する
- エラーチェックする
- エラーだったら終了
- 理科の点数を取得する
- エラーチェックする
- エラーだったら終了
- 社会の点数を取得する
- エラーチェックする
- エラーだったら終了
- 英語の点数を取得する
- エラーチェックする
- エラーだったら終了
- とった値をシートに出力する
さて、これを見て何か?思いませんか?何もおかしなところがないと思った人はもう一度思い出しましょう。
プログラムはできるだけ同じ処理をしない!
これ、取得してチェックして、取得してチェックしてを今回は5回も繰り返してます。後の回でもやりますがこれ科目が10科目になったら実際どうします?10回チェックしますか?まぁ10回チェックはするんですがw(ぉい)
問題はその処理の順番です。実はこれは非常にわかりにくプログラムです。なぜなら終了する箇所がそこかしこに埋まっているからです。今は点数だけなのでいいですが、他の要素やもう少し複雑になったら、「どこで終了したのか?」が実は結構わかりにくくなってしまいます。繰り返しますが、このプログラムでも「問題はありません」ですが、私は美しくない。と考えている。ということです。こんな処理はどうでしょうか?
- 出席番号を取得する
- 出席番号と各科目の点数をチェックする
- エラーだったら終了
- 各科目の点数をシートに出力する
これでどうなったでしょうか?とても簡単になってませんか?何?書き方が各科目になっただけ?ではもう少し詳しく!
- 出席番号を取得する
- 出席番号をチェックする
- 算数の点数をチェックする
- 国語の点数をチェックする
- 理科の点数をチェックする
- 社会の点数をチェックする
- 英語の点数をチェックする
- エラーだったら終了
- とった値をシートに出力する
どうでしょうか?こう書いてもやっぱり簡単になっています。まとめて処理をする。順番を変える。たったこれだけでこんなにもすっきりとしてしまいます。これを実現するには構造体を覚える必要があります。とうとう、応用編です。確かに構造体はもしかしたら理解しにくいかもしれない。でも覚えればこれほど強力なものはありません。正式にVBAではこれを「ユーザー定義型」と呼んでいます。
ユーザー定義型はいくつかの変数をまとめたもの!
一言で言うと表題の通りなんですが・・・普通の変数には1つの型で1つの値を入れることしかできません。複数の値を入れたいなら配列を使っていましたね。しかし配列も1つの型の複数のデータを扱っています。では異なる型で複数の値を扱いたい場合はどうしたらいいでしょうか?
科目が増えたらどうしよう?
それを扱えるのがユーザー定義型です。今は科目名の変数があり、各点数の値は算数用、国語用と科目ごとに用意しています。10科目になったら10個変数をつくらなければいけません。これは正直ちょっと管理しにくいです。科目も点数もどれだけ増えてもへっちゃらにしたいですよね?さっきの説明を思い出しましょう。「ユーザー定義型は異なる型の複数の値を扱えます。」
- 科目の名前(文字列)
- 科目の点数(汎用型)
はいぴったりです!ユーザー定義型を使えば科目の名前と点数を同時に扱うことができます。では早速書き方です。mdlDefineに新しいユーザー定義型「Kamoku」を作ります。ついでに科目は複数あるので科目の配列も作成しましょう。
' 科目のユーザー定義型 Public Type Kamoku Name As String '科目名 Score As Variant '点数 End Type
点数をVariant型にしているのは、点数に文字列が入ってもVariantならエラーにならないからです。
さらに、標準モジュールmdlMainに以下のコードを追加します。
' 科目の配列を作成します。 Dim arryKamoku() As Kamoku
これでOKです。これで次回から大きく!処理の順番を改善していきます。だんだん難しくなっていますが、頑張ってついてきてください。ちなみに、私が作ったマクロをそのままファイルとして載せてダウンロードできるようにはしません。なぜなら「コードを書いてほしい」からです。まったく同じでもいいです。コピペしないで是非!書いてください。
今日はここまで!
かしこ