VBAの勉強を始めてみた

色々試しています。

日付データ(シリアル値)をお好みの表示形式で文字列に変換する

今回は、日付データを一括で文字列に変換する簡単なマクロを作ったので紹介します。

 

まず、日付データを文字列として扱いたいのはどんな場合か? について、軽く触れておきたいと思います。

 

このように、A列に日付データ、B列にコメントがあったとして

f:id:kouten0430:20180710143217j:plain

 

これを、C列に文字列結合 =A1&"は"&B1 すると

f:id:kouten0430:20180710143322j:plain

 

あれ?何だこの数値?(@_@;)

 

これは、日付データの実体はシリアル値だからであり、表に見えている 7月10日 などは、エクセルが私達に見せている仮の姿です。
表示形式を変えると、エクセルが 43291 のようなシリアル値に、表示形式ごとの仮面を被せて

7/10
2018/7/10
2018年7月
2018年7月10日
H30.7.10
平成30年7月10日
・・・等

のように、見えるようにしています。

 

このような日付データを、シリアル値ではなく、純粋な "7月10日" という文字列にしてから文字列結合を行うとうまくいきます。

f:id:kouten0430:20180710143358j:plain

 

では、日付データを文字列として扱うにはどうすればいいでしょう?
この場合、次のような方法が考えられると思います。

  • A列の日付データを文字列に変換する
  • TEXT関数を使って文字列結合する。(C列の式を =TEXT(A1,"m月d日")&"は"&B1 のようにする)

 

TEXT関数なんてチマチマ使ってられるか。バーロー!

という人のために、A列の日付データを一括で文字列に変換する という方法をマクロで提供しようと思います。(もちろんA列じゃなくてもOKです)
ただし、日付データを文字列に変換した場合、日付データとしての計算や判定ができなくなるので注意が必要です。

Sub 日付データを好きな書式で文字列に変換()
    Dim myRange As Range
    Dim n As Integer
    
    n = Application.InputBox(prompt:="書式を選んで下さい" & vbCrLf & vbCrLf & _
    "1:yyyy/m/d" & vbCrLf & "2:yyyy/m" & vbCrLf & "3:yy/m/d" & vbCrLf & _
    "4:m/d" & vbCrLf & "5:yyyy年m月d日" & vbCrLf & "6:yyyy年m月" & vbCrLf & _
    "7:yy年m月d日" & vbCrLf & "8:m月d日" & vbCrLf & "9:ggge年m月d日" & vbCrLf & _
    "10:ggge年m月" & vbCrLf & "11:ge年m月d日" & vbCrLf & "12:ge年m月" & vbCrLf & _
    "13:ge/m/d" & vbCrLf & "14:ge/m" & vbCrLf & vbCrLf & "(9~14は和暦です)" & _
    vbCrLf & vbCrLf, Type:=1)
        If TypeName(n) = "Boolean" Or n < 1 Or n > 14 Then
            Exit Sub
        End If
    
    For Each myRange In Selection.SpecialCells(xlCellTypeVisible)   '可視セルのみに処理を行う
        If myRange.Address = myRange.MergeArea(1).Address Then   '結合セルの場合は左上のセルのみ処理を行う
            Select Case n

            Case 1
                myRange.Value = "'" & Format(myRange.Value, "yyyy/m/d")
            Case 2
                myRange.Value = "'" & Format(myRange.Value, "yyyy/m")
            Case 3
                myRange.Value = "'" & Format(myRange.Value, "yy/m/d")
            Case 4
                myRange.Value = "'" & Format(myRange.Value, "m/d")
            Case 5
                myRange.Value = "'" & Format(myRange.Value, "yyyy""年""m""月""d""日""")
            Case 6
                myRange.Value = "'" & Format(myRange.Value, "yyyy""年""m""月""")
            Case 7
                myRange.Value = "'" & Format(myRange.Value, "yy""年""m""月""d""日""")
            Case 8
                myRange.Value = "'" & Format(myRange.Value, "m""月""d""日""")
            Case 9
                myRange.Value = "'" & Format(myRange.Value, "ggge""年""m""月""d""日""")
            Case 10
                myRange.Value = "'" & Format(myRange.Value, "ggge""年""m""月""")
            Case 11
                myRange.Value = "'" & Format(myRange.Value, "ge""年""m""月""d""日""")
            Case 12
                myRange.Value = "'" & Format(myRange.Value, "ge""年""m""月""")
            Case 13
                myRange.Value = "'" & Format(myRange.Value, "ge/m/d")
            Case 14
                myRange.Value = "'" & Format(myRange.Value, "ge/m")
                
            End Select
        End If
    Next myRange
End Sub

 

※コードの使用方法

  1. SubからEnd Subまでをコピーし、標準モジュール等に貼り付けて使用して下さい。なお、マクロで実行した処理は「元に戻す」ことができません。実行前に一旦保存しやり直しのできる状態にしておいて下さい。標準モジュールにコードを貼り付けてマクロを使用する方法はこちら
  2. 文字列に変換したい日付データの入ったセルを選択します

    f:id:kouten0430:20180710144851j:plain(変換前はシリアル値なので、右詰め表示です)

  3. マクロを実行します
  4. 表示されたInputBoxからお好きな書式を選んでOKします

    f:id:kouten0430:20180710144939j:plain(変換後は文字列なので、左詰め表示です)

 

 そもそも書式記号が分からない!という人は、過去記事で触れていますので参考に。 

kouten0430.hatenablog.com

プルダウンで選択すると、シート内の指定セルにジャンプするイベントプロシージャ

今回は、プルダウンメニューから選んだ文字列と同じ文字列を含む、同一シート内のセルにジャンプする(画面の一番上に表示させる)イベントプロシージャを紹介します。

 

プルダウンメニューから文字列を選ぶ度に、同じ文字列を含むセルを

f:id:kouten0430:20180630172203j:plain

 

このように一番上に表示されるようにしたいと思います。

f:id:kouten0430:20180630165355j:plain

 

VBAでは、ある操作をトリガーとして自動的に実行されるプロシージャを「イベントプロシージャ」と呼びます。自動実行のトリガーとなる操作のことを「イベント」といい、今回は、プルダウンメニュー(が設置されたセル)の内容を変化させる度に、

シート内の指定範囲を検索し、検索に一致するセルを一番上に表示させる・・・・・・。

というプログラムを自動実行させてみたいと思います。

 

ワークシートのセルがユーザー(または外部リンク)によって変更されたときに、プロシージャが自動的に実行されるようにするには、Worksheet_Change というイベントプロシージャを使用します。

Private Sub Worksheet_Change(ByVal Target As Range)
    Dim myRange As Range
    
    If Target.Address <> "$A$1" Then Exit Sub
    
    Set myRange = Range("A2:A1048576").Find(Target.Value, LookAt:=xlWhole)
    
    ActiveWindow.ScrollRow = myRange.Row
    
End Sub

 

※プログラムの解説

  • Worksheet_Change という名前のイベントプロシージャを記述します。Worksheetというオブジェクトで、Change(変化)というイベントがあった時に自動実行されるプロシージャを示します。イベントプロシージャ名は、ユーザーが自由に変更することはできません。
  • 引数の指定はイベントプロシージャごとに決まっており、Worksheet_Change では(ByVal Target As Range)とします。ワークシート内で変更があったセル(Range)が Target に格納されます。変数名の Target は任意ですが、引数の渡し方を指定する ByVal(※1)と、変数の型を指定する As Range は変更不可です。
  • Ifで、変更があったセルのアドレスが$A$1(つまり、プルダウンメニューを設置したセル)以外であった場合は、プロシージャを終了し、何もしません。
  • Findメソッドで、A2:A1048576(つまり、ジャンプ先の見出しが存在するセル範囲)を、プルダウンメニューで選択した文字列で検索し、一致したセル(Range)を返します。LookAt:=xlWhole の引数で、検索条件を完全一致にしています。
  • ActiveWindow.ScrollRowで、検索に一致したセルのある行を上端に表示させます。ウィンドウ枠を固定している場合、 固定領域は対象外となります。


※コードの使用方法

  • Private SubからEnd Subまでをコピーし、イベントが発生するオブジェクトモジュールに貼り付けて使用して下さい。つまり、book1のSheet1で発生するイベントに対してであれば、VBAProject(Book1) - Microsoft Excel Objects - Sheet1 モジュールに記述します。標準モジュールや、個人用マクロブックに記述しても実行されません。

    f:id:kouten0430:20180630170704j:plain

同様の処理を、Workbook_SheetChangeというイベントプロシージャでも作成することができます。その場合は、ThisWorkbookモジュールにコードを記述します。Workbook_SheetChangeは、Workbookというオブジェクト(つまり、Book内の全てのシート)で、SheetChangeというイベントがあった時に自動実行されるプロシージャです。(Worksheet_Changeは一つのシートのみが対象)

 

※1
ByValは、引数が値渡しになります。参考に、省略またはByRefは、引数が参照渡しになります。ValはValue(値)の略で、RefはReference(参照)の略。
値渡しと参照渡しについては、thom (id:t-hom) さんのこちらの記事でわかりやすく解説されています。

thom.hateblo.jp

ループ処理を極力軽くする方法の模索~混乱編~(Tips-7)

VBAの勉強を始めて、これまでに学んだことや、思いついたアレやコレをメモっておきます。

 

今回は、ループ処理を使わなければならない場合に、ループの中に含まれる処理を極力軽くする方法について、模索してみようと思います。

下のように、1~9の数値がランダムに入った、A1:A10000のセル範囲に対して、数値が偶数だった場合に行を非表示にする処理を、いくつかのコードで試して、速さの違いを比較してみましょう。

f:id:kouten0430:20180624151654j:plain


test1として、セルを上から順に比較していき、偶数だった場合にその都度、行を非表示にします。
(Modは、左側の数値を右側の数値で割って、余りを求める演算子です。なので、2で割った時に0であった場合が偶数ということになります)
※時間計測のために必要なコードは省略して記載します。

Sub test1()
    Dim i As Integer
    
    For i = 1 To 10000
        If Cells(i, 1).Value Mod 2 = 0 Then
            Rows(i & ":" & i).Hidden = True
        End If
    Next i
    
End Sub

 

 上記のコードでかかった時間は、

f:id:kouten0430:20180624150823j:plain

7秒

 

次に、test2として、セルを上から順に比較していき、偶数だった場合にその都度、行をUnionメソッドで取り込み、最後にまとめて非表示にします。

Sub test2()
    Dim i As Integer
    Dim myRange As Range
    
    For i = 1 To 10000
        If Cells(i, 1).Value Mod 2 = 0 Then
            If myRange Is Nothing Then
                Set myRange = Range(i & ":" & i)
            Else
                Set myRange = Union(myRange, Range(i & ":" & i))
            End If
        End If
    Next i

    myRange.EntireRow.Hidden = True

End Sub

 

上記のコードでかかった時間は、

f:id:kouten0430:20180624150909j:plain

56秒

あれ?

 

最後にまとめて、非表示にするほうが速いと思っていたんですが・・・・・・。

 

と、とりあえず、次に、test3として、A1:A10000の範囲を配列に取り込んでから比較し、偶数だった場合にその都度、行をUnionメソッドで取り込んで、最後にまとめて非表示にします。

Sub test3()
    Dim i As Integer
    Dim myRange As Range
    
    Tdim = Range("A1:A10000")
    
    For i = 1 To 10000
        If Tdim(i, 1) Mod 2 = 0 Then
            If myRange Is Nothing Then
                Set myRange = Range(i & ":" & i)
            Else
                Set myRange = Union(myRange, Range(i & ":" & i))
            End If
        End If
    Next i

    myRange.EntireRow.Hidden = True
    
End Sub

 

上記のコードでかかった時間は、

f:id:kouten0430:20180624151057j:plain

56秒

変わらない・・・・・・。

 

最後に、test4として、A1:A10000の範囲を配列に取り込んでから比較し、偶数だった場合にその都度、行を非表示にします。

Sub test4()
    Dim i As Integer
    Dim myRange As Range
    
    Tdim = Range("A1:A10000")
    
    For i = 1 To 10000
        If Tdim(i, 1) Mod 2 = 0 Then
            Rows(i & ":" & i).Hidden = True
        End If
    Next i

End Sub

 

上記のコードでかかった時間は、

f:id:kouten0430:20180624151130j:plain

7秒
test1と変わらないね(@_@;)

 

test1~4をまとめると、下表のようになりました。

方法 時間
1セルごとに比較し、1行ずつ非表示にする 7秒
1セルごとに比較し、Unionメソッドで取り込んだ行を最後にまとめて非表示にする 56秒
配列の中で比較し、Unionメソッドで取り込んだ行を最後にまとめて非表示にする 56秒
配列の中で比較し、1行ずつ非表示にする 7秒

 

えーっと。
自分の予想では、速い順に、test3 < test2 < test4 << test1 だったのですが。
すべて予想と違い、若干混乱しております(@_@;)

Unionメソッドってもしかして、時間のかかる処理なのかな?

セル比較と配列比較で時間が変わらなかったのは、サンプルデータが少なかったからでしょうか。それとも何か、見落としていることがある?

 

というか、今日は何を模索するんだっけ?あれ??

指定した数の行を一瞬で挿入するには?(Tips-6)

VBAの勉強を始めて、これまでに学んだことや、思いついたアレやコレをメモっておきます。

 

今回は、VBAで、ユーザーが指定した数の行を挿入する方法についてです。

まず、下のように、For ~Next を使って、指定した数の行を挿入させてみようと思います。
アクティブセルがある行の上方向に、行挿入する処理を n回 繰り返します。

Sub test1()
    Dim n As Long

    n = InputBox("挿入する行数を入力して下さい")
    
    For i = 1 To n
        ActiveCell.EntireRow.Insert xlShiftDown, xlFormatFromLeftOrAbove
    Next i

End Sub

 

1~1,000行程度であれば、ほぼ一瞬で完了しますが、10,000行辺りから少し時間がかかるようになってきます。100,000行になると・・・・・・、

f:id:kouten0430:20180623150309j:plain

3分13秒

うーん。カップラーメンが出来てしまいますぅ(@_@;)

 

上記は、VBAの勉強を始めた初期の頃に作ったコードです。今見ると、腹パンしてやりたくなりますね。
前回も書きましたが、ループというのは掛け算であり、処理の内容によっては、とんでもなく時間がかかってしまうんです・・・・・・。

実際に100,000行も挿入するようなことはあまりありませんが、私はこのコードが気に入らず、コードを書いた当日はずっとモヤモヤした気分でした。

 

しかし、翌日ふと、次のような処理を思いつき、コードを修正しました。

Sub test2()
    Dim n As Long

    n = InputBox("挿入する行数を入力して下さい")

    Rows(ActiveCell.Row & ":" & ActiveCell.Row + n - 1).Insert xlShiftDown, xlFormatFromLeftOrAbove

End Sub

 

アクティブセルがある行から下に n行 選択した状態で、行挿入を1回だけ行うのです。単純ですね。


エクセルの操作に例えるとこんな感じ、

f:id:kouten0430:20180623150424j:plain

2~11行目を(要するに2行目を始点にして10行分)選択した状態で「挿入」します。

 

f:id:kouten0430:20180623150539j:plain

すると、1行目と2行目の間に10行挿入されます。

上記のコードでやっているのは、これと同じようなことです。

 

このコードで、100,000行挿入した時の時間は・・・・・・、

f:id:kouten0430:20180623150643j:plain

0秒

なーんだ、ループを使わなくてもできる方法があるじゃないか。


test1,test2 について、処理にかかった時間をまとめると下表のようになりました。

挿入する行数 test1 test2
10 0秒 0秒
100 0秒 0秒
1,000 1秒 0秒
10,000 9秒 0秒
100,000 3分13秒 0秒
1,000,000 計測不能 0秒

 

これで、test1のコードは実用に耐えないことが分かります。ある程度、VBAに慣れてくると、test1のようなコードは実行するまでもなく、時間がかかることが分かるようになってくるので、最初から別の方法でコーディングすることを考えるようになってきます。

 

 初心者の私は、こんな感じで処理を効率化できたときの嬉しさが忘れられず、そして、これからも味わうために、プログラミングにハマっているのかもしれません。

 

今回のような例は、ExcelVBA ならではのものなのかもしれませんが(@_@;)

 

前回はワークシート関数でループ処理を代替し、今回はその他の方法でループ処理を代替してみました。次回は、ループをどうしても使わなければならない場合に、処理をできるだけ軽くする方法などを模索してみようと思います。

空白以外のセルを一瞬でカウントするには?(Tips-5)

VBAの勉強を始めて、これまでに学んだことや、思いついたアレやコレをメモっておきます。

 

今回は、VBAで、セルにデータが有るか無いか(または特定のデータが有るか無いか、でもいい)を調べて、そのデータ数をカウントする方法についてです。

まず、下のように、For Each ~Next を使って A1:Z1048576 の範囲にデータがいくつ存在しているかをカウントしてみようと思います。
セルが空白ではない場合に、cnt を +1 します。

Sub test1()

    For Each myRange In Range("A1:Z1048576")
        If myRange.Value <> "" Then
            cnt = cnt + 1
        End If
    Next myRange
    
End Sub

 

おっと、その前に、処理にどのくらい時間がかかったのかを調べるために、時間計測できる仕掛けを組み込んでおきましょう。

Sub test1()

    StartTime = Time

    For Each myRange In Range("A1:Z1048576")
        If myRange.Value <> "" Then
            cnt = cnt + 1
        End If
    Next myRange
    
    StopTime = Time
    MsgBox "処理にかかった時間は" & vbCrLf & Minute(StopTime - StartTime) _
    & "分" & Second(StopTime - StartTime) & "秒"
    
End Sub

 

さあ、これでかかった時間は・・・・・・、

f:id:kouten0430:20180616165838j:plain

1分58秒

・・・・・・結構、かかったね(@_@;)

 

次は、A1:Z1048576 の範囲を配列に代入してから、配列をループして調べる方法を試してみます。配列の中身が空白ではない場合に、cnt を +1 します。

Sub test2()

    StartTime = Time

    Tdim = Range("A1:Z1048576")
    
    For i = 1 To 1048576
        For j = 1 To 26
            If Tdim(i, j) <> "" Then
                cnt = cnt + 1
            End If
        Next j
    Next i
    
    StopTime = Time
    MsgBox "処理にかかった時間は" & vbCrLf & Minute(StopTime - StartTime) _
    & "分" & Second(StopTime - StartTime) & "秒"
    
End Sub

 

かかった時間は・・・・・・、

f:id:kouten0430:20180616170038j:plain

5秒

圧倒的に速くなりましたよ!

 

この結果から、配列へのアクセスが速いこと(言い換えればループの都度、セルへアクセスすることが時間のかかること)が分かります。
ループというのは掛け算であり、ループの中に入っている処理が少し重いだけで、ループ回数によってはとんでもない時間を要してしまうんですよね・・・・・・。

 

でも、セルにデータが有るか無いかを調べるのであれば、もっと速い方法がありますよね?

それは、ワークシート関数を利用することです。test1、test2と同じ処理をワークシート関数を使って、書き直してみましょう。
CountIfで、空白以外のセル数をカウントさせます。

Sub test3()

    StartTime = Time

    cnt = WorksheetFunction.CountIf(Range("A1:Z1048576"), "<>")
    
    StopTime = Time
    MsgBox "処理にかかった時間は" & vbCrLf & Minute(StopTime - StartTime) _
    & "分" & Second(StopTime - StartTime) & "秒"
    
End Sub

 

かかった時間は・・・・・・、

f:id:kouten0430:20180616170224j:plain

0秒

速い!しかも、コードがすっきりするというオマケつき。

 

test1~3について、それぞれにかかった時間をまとめると下のようになります。 

方法 時間
For Each ~Next で1セルずつ調べる 1分58秒
配列に格納してから調べる 5秒
ワークシート関数で調べる 0秒

 

こうやって見ると、ワークシート関数が圧倒的に速いんですよね。

いや、けして、ワークシート関数利用推進派の回し者ではありませんよ。

 

おまけ:
今回のコードでは、A1:Z1048576 のセル範囲を試験的に調べましたが、じゃあ、A1:XFD1048576 の範囲(要するにシート内のすべてのセル)だったらどのくらい時間がかかるの?ということで、補足しておきます。
私のPCスペックでは、test1のケースで、単純計算で20時間かかり、test2のケースでは、メモリ不足のためテストすることすらできません。
しかし、test3のワークシート関数であれば、なんと!それでも 0秒 で処理が完了してしまいます。普段、ワークシート関数を使う分には何の疑問も抱きませんが、こうやって他の方法と比較してみると、「どうやって処理してるの?」と不思議に思ったりもします。

 

次回は、ワークシート関数以外で、ループ処理をなるべく使わずに済む方法を模索したり、ループ処理をどうしても使わなければならない場面で、ループ回数の掛け算の被乗数側となる処理をできる限り軽くする方法などについて考えてみたいと思います(@_@;)

InputBoxのキャンセル判定について(Tips-4)

VBAの勉強を始めて、これまでに学んだことや、思いついたアレやコレをメモっておきます。

 

今回は、InputBoxでキャンセルを押された場合の判定についてです。

InputBoxでキャンセルを押されたときに、何らかの処理(例えばプロシージャを終了するとか)をさせるために、下記のような条件分岐を入れることがあると思います。

Sub test()
    tmp = InputBox("何か入力して下さい")
        If tmp = "" Then
            Exit Sub
        End If
End Sub

 

InputBox関数でキャンセルを押された場合の戻り値は、空白(長さ0の文字列)です。
なので、変数の中身が空白であった場合は、条件分岐で Exit Sub をさせるわけです。

では、戻り値に空白そのものを使用したい場合はどうでしょう?
上記のコードでInputBoxを空白のままでOKしても、結局、変数の中身は空白であり、キャンセルと同じく Exit Sub されてしまいます。

 

このようなケースでは、InputBox関数ではなく、InputBoxメソッドを使用すると便利です。

InputBoxメソッドでキャンセルを押された場合は、戻り値にBoolean型の False が返ってきますので、変数の型が Variant であれば、下記のように条件分岐させることができます。

Sub test()
    tmp = Application.InputBox("何か入力して下さい")
        If TypeName(tmp) = "Boolean" Then
            Exit Sub
        End If
End Sub

 

TypeName関数で変数のデータ型を調べ、Boolean型であれば条件分岐で Exit Sub するようにしています。

では、変数をVariant型ではなく、数値型や文字列型などで宣言した場合はどうなるでしょうか?
やってみましょう。
変数を数値型にして、キャンセルした場合は False に該当する数値の 0 が入るので、

Sub test()
    Dim tmp As Integer
    
    tmp = Application.InputBox("何か入力して下さい")
        If tmp = 0 Then
            Exit Sub
        End If
End Sub

 

のように条件分岐させることができます。

ではでは、変数を文字列型にした場合はどーでしょう?
変数を文字列型にして、キャンセルした場合は False という文字列が入るので、

Sub test()
    Dim tmp As String
    
    tmp = Application.InputBox("何か入力して下さい")
        If tmp = "False" Then
            Exit Sub
        End If
End Sub

 

のように条件分岐させることができます。

ただし、上記二つの例は 0 という数値と、False という文字列が戻り値として扱えなくなってしまうため、理由がなければ、変数は Variant で宣言すれば良いでしょう。

 

ちなみに、InputBoxメソッドはモードレスなので、入力する数値や文字列をセルから選ぶこともできます。

  • モードレス:ダイアログボックスが表示されている間、ダイアログボックス以外の操作もできる状態
  • モーダル:ダイアログボックスが表示されている間、ダイアログボックス以外の操作ができない状態(InputBox関数で表示されるダイアログボックスはモーダル)

 

ダイアログボックスが表示されている状態で、セルを範囲選択すれば、Range(セル範囲)を入力することもできます。
しかし、戻り値をRange型(要するにオブジェクト)にする場合は、必ず先頭に Set をつけて、

Set tmp = Application.InputBox(prompt:="何か入力して下さい", Type:=8)

のように、書く必要があるため、この時点でBoolean型のデータは代入することができなくなってしまいます。
何を言いたいかというと、この Set がついていると、キャンセルしたときに「オブジェクトが存在しないぜ」ってことで、エラーが発生するってことです。

うーん・・・・・・。じゃあ、キャンセル判定どーしようか?

エラーになるっていうことは、変数は空っぽのままのハズなので、次のようにしてみましょう。

Sub test()
    Dim tmp As Range
    
    Set tmp = Application.InputBox(prompt:="何か入力して下さい", Type:=8)
        If tmp Is Nothing Then
            Exit Sub
        End If
End Sub

 

tmp Is Nothing で、オブジェクト変数が空っぽ(Nothing)ならば、条件分岐で Exit Sub させます。

 

え?・・・・・・なんか足りない?

そもそも、エラーが発生した行で処理が止まってしまうので、以下の記述をさらに追加する必要があります。

Sub test()
    Dim tmp As Range
    
    On Error Resume Next
    
    Set tmp = Application.InputBox(prompt:="何か入力して下さい", Type:=8)
        If tmp Is Nothing Then
            Exit Sub
        End If
        
    On Error GoTo 0
    
End Sub

 

On Error Resume Next から下の行は、エラーが発生しても、エラーを無視してそのまま処理を続けるようになります。

そして、On Error GoTo 0 で、On Error Resume Next の効果を解除します。

 

これで、Range型の場合のキャンセル判定もできるようになりましたね! 

 

※Range型のキャンセル判定のケースは インストラクターのネタ帳 というサイトを参考にさせていただきました。

多重ループを一気に抜けるには?(Tips-3)

VBAの勉強を始めて、これまでに学んだことや、思いついたアレやコレをメモっておきます。

 

タイトルのとおりですが、コーディングしていると、入れ子になったループの中から一気に抜け出したい場面があります。
しかし、下記のように、一番内側のループの中で Exit For しても、ループを抜けるのは該当の For ~Next だけで、外側のループまでは抜けることができません。

Sub test()
    For i = 1 To 10
        For j = 1 To 10
            For k = 1 To 10
                If k = 5 Then
                    Exit For
                End If
            Next k
        Next j
    Next i
End Sub

 

こういうとき私は、初心者モード全開で、GoTo を乱用するのですが、

Sub test()
    For i = 1 To 10
        For j = 1 To 10
            For k = 1 To 10
                If k = 5 Then
                    GoTo skip
                End If
            Next k
        Next j
    Next i
    
skip:

End Sub

 

ネットでいろいろ調べていると、可読性が下がるとか、うんぬんとかであまり好まれていないようです。

じゃあどうすればいいのさっ!?ということで、別の方法を学んでみました。

 

Exit For と一緒に、外側のループ終了条件も満たしてあげる方法

Sub test()
    For i = 1 To 10
        For j = 1 To 10
            For k = 1 To 10
                If k = 5 Then
                    i = 10
                    j = 10
                    Exit For
                End If
            Next k
        Next j
    Next i
End Sub

 

フラグを True にし、フラグが True なら条件分岐で外側のループも Exit For する方法

Sub test()
    For i = 1 To 10
        For j = 1 To 10
            For k = 1 To 10
                If k = 5 Then
                    flag = True
                    Exit For
                End If
            Next k
        If flag Then Exit For
        Next j
    If flag Then Exit For
    Next i
End Sub

 

他にもいろいろあると思いますケド、初心者的には、ぱっと見、GoTo が一番わかりやすいように思えるのは気のせい?
でも、一つのコードの中で GoTo を多用すれば、スパゲッティのようにコードが絡み合い、可読性が下がってしまうことでしょう。

どれが正統なやり方なの?って思いますが、組織で決まったお作法(指針?)などがあれば、それに従い、個人レベルであれば本やネットを参考に汎用性やメンテナンス性を高めたり、ミスを防いだりするための、個人ルールを構築するって感じでしょうか。
うーん。まだまだ知らないことが多くありそうですが・・・・・・、「お作法」の存在を気にしすぎて、自由な発想が妨げられないようにもしたい今日この頃であります。