ExcelVBAマクロのチートシート

# はじめに

Excel 2019 on Windows10 を対象としています。
公開されている情報を参照して、プライベートの時間に整理したものです。
順次追記予定です。

# 関連記事

- [ExcelVBAマクロの開発・実行環境を準備する](https://qiita.com/yikemac/items/3b99ccb032faab552045)

# 元に戻す／やり直しへの対応

Excel では VBA マクロの各種関数でシート内容を加工してしまうと、マクロ実行後に元に戻す／やり直しの履歴がリセットされてしまい、マクロ実行前の状態に戻せなくなることがあります。このセクションでは、履歴を維持したい場合の対応方法と注意点について記載します。

- SendKeys によりショートカットキーを実行した場合には、元に戻す／やり直しの履歴が維持されるため、シート内容を加工する処理には SendKeys を用いるとよい
- マクロ実行中に SendKeys の操作結果を反映するためには DoEvents を2回実行する。SendKeys の Wait オプションは有効に機能しない。DoEvents を3回以上実行すると履歴がリセットされる場合がある。サブルーチンの最初に DoEvents を実行した場合は挙動がおかしくなる。これらの条件は環境に依存しているかもしれない
- Application.ScreenUpdating や Application.CutCopyMode を変更すると、同様に履歴がリセットされる場合があるので、履歴を維持したい場合は動作確認をするか、なるべく変更を避ける
- ボタンを連続クリックすると誤動作する場合があるが、VBA では厳密な再入防止はできないので、時間間隔をあけて操作してもらう
- 参考：[SendKeys ステートメント](https://docs.microsoft.com/ja-jp/office/vba/language/reference/user-interface-help/sendkeys-statement)

```vb
Public Sub 次の行を挿入()
    SendKeys "{DOWN}"
    DoEvents
    DoEvents
    
    Selection.EntireRow.Select
    
    SendKeys "^{+}"
    DoEvents
    DoEvents
End Sub
```

- VBAによるセルの値変更を伴う処理では、SendKeys の結果が反映されるタイミングと値変更のタイミングがずれてしまうことがあるので、元に戻せなくなることを許容して、各種関数での加工をすることになる
- 元に戻せない操作を伴う場合は、事前に確認メッセージを表示するとよい

```vb
Public Sub 次の行を挿入2()
    ' 元に戻すことができなくなることの確認をする
    If MsgBox("元に戻す操作はできませんが、実行しますか", vbOKCancel, "次の行を挿入2") = vbCancel Then
        Exit Sub
    End If
    Selection.EntireRow.Offset(1, 0).Insert  ' 次の行を挿入する    
    Selection.EntireRow.AutoFit  ' 行幅を自動調整する
End Sub
```

# 選択中セルの操作

Selection により選択中セルの Range オブジェクトを取得して操作できます。

- 参考：[Application.Selection プロパティ](https://docs.microsoft.com/ja-jp/office/vba/api/excel.application.selection)
- 参考：[Range オブジェクト](https://docs.microsoft.com/ja-jp/office/vba/api/excel.range(object))

```vb
    ' 選択中でなければ Nothing になっている
    If Selection Is Nothing Then
        Exit Sub
    End If

    ' アドレスを取得する
    ' 例えば、単一セル選択時は"$A$1"、複数セル選択時は"$A$1:$C$3"となる
    Debug.Print Selection.Address

    ' 選択範囲の各セルに対して繰り返す
    Dim cell As Variant  ' セル
    For Each cell In Selection
        Debug.Print cell.Address
    Next

    ' 選択範囲の各行に対して繰り返す
    Dim row As Variant
    For Each row In Selection.rows
        Debug.Print "ROW:" & row.Address
        For Each cell In row.Cells
            Debug.Print cell.Address
        Next
    Next

    ' 選択範囲の左上のセルのみ選択する
    ' Cells(行, 列)により選択範囲における相対位置を指定して単一セルを選択できる
    ' 左上が(1, 1)となる
    Selection.Cells(1, 1).Select

    ' テキストを取得する
    Debug.Print Selection.Cells(1, 1).Text
    Debug.Print Selection.Cells(1, 1)  ' 受け取り側が String 型であれば .Text は省略できます

    ' セルの値を設定する
    Selection.Cells(1, 1) = "ABC"

    ' セル位置はインデクスでも指定できる。例えば2行3列を選択時は下記に対応する
    ' | 1 | 2 | 3 |
    ' | 4 | 5 | 6 |
    ' セル位置を指定しない場合は、複数セル選択時にNullになる
    Debug.Print Selection(1)

    ' 元の選択範囲を保持する。Setが必要
    Dim org As Range  ' 元の選択範囲
    Set org = Selection

    ' 次の行を選択する
    ' Offset(行, 列)により相対位置を指定してずらし、Selectにより選択できる
    ' このとき、元の選択範囲のサイズが維持されるが、結合セルがあると選択範囲が変わる
    ' 元に戻す／やり直しの履歴は維持されるが、Selectによる選択の操作は履歴に含まれない
    Selection.Offset(1, 0).Select

    ' 行全体を選択する
    Selection.EntireRow.Select

    ' 列全体を選択する
    Selection.EntireColumn.Select

    ' 行全体をコピーして次の行に挿入する
    ' このとき、元に戻す／やり直しの履歴は維持される
    Selection.EntireRow.Copy
    Selection.EntireRow.Offset(1, 0).EntireRow.Select
    SendKeys "^{+}"
    DoEvents
    DoEvents

    ' 元の選択範囲に戻す
    org.Select
```

# 使用範囲の選択

ActiveSheet.UsedRange により使用範囲の Range オブジェクトを取得して操作できます。

- 参考：[Worksheet.UsedRange プロパティ](https://docs.microsoft.com/ja-jp/office/vba/api/excel.worksheet.usedrange)

```vb
    ' 使用範囲を選択する
    ActiveSheet.UsedRange.Select

    ' 使用範囲の右上のセルを選択する
    With ActiveSheet.UsedRange
        .Cells(1, .Columns.Count).Select
    End With
```

# アドレス指定による操作

ActiveSheet.Range によりアドレスを指定して操作できます。

- 参考：[Range オブジェクト](https://docs.microsoft.com/ja-jp/office/vba/api/excel.range(object))

```vb
    ' E3セルのテキストを表示
    Debug.Print ActiveSheet.Range("E3")

    ' E3:E5を範囲選択する
    ActiveSheet.Range("E3:E5").Select
```

# 重なる範囲の取得

Intersect を用いて範囲のAND条件を求められます。

- 参考：[Application.Intersect メソッド](https://docs.microsoft.com/ja-jp/office/vba/api/excel.application.intersect)

```vb
    ' 同じ行の別の列を選択
    Debug.Print Intersect(Range("N10").EntireRow, Range("L:L")).Address  ' "$L$10"が表示される

    ' 全行を対象にすると時間がかかるので、使用範囲のE列に対してのみ処理することで、処理時間を短縮する
    Dim cell As Range  ' セル
    For Each cell In Intersect(ActiveSheet.UsedRange, ActiveSheet.Range("E:E"))
        Debug.Print cell.Address
    Next
```

# クリップボードの操作

下記の関数を作成して呼び出すことでテキストをコピー／取得できます。

- 同一モジュールからのみ呼び出す場合は Public → Private にします
- 参考：[[VBA]DataObjectを使ったクリップボード操作が上手くいかない場合の対処法](https://www.ka-net.org/blog/?p=7537)

```vb
Public Sub SetTextToClipboard(text As String)
    ' テキストをクリップボードにコピーする
    With CreateObject("Forms.TextBox.1")
        .text = text
        .SelStart = 0
        .SelLength = .TextLength
        .Copy
    End With
End Sub

Public Function GetTextFromClipboard() As String
    ' クリップボードからテキストを取得する
    With CreateObject("Forms.TextBox.1")
        .MultiLine = True
        If .CanPaste = True Then .Paste
        GetTextFromClipboard = .text
    End With
End Function
```

# テキストの判定

Like 演算子を活用することで、テキストの判定が容易になります。

- 参考：[Like 演算子](https://docs.microsoft.com/ja-jp/office/vba/language/reference/user-interface-help/like-operator)

```vb
    ' 選択中セルのテキストを取得
    Dim text As String  ' 選択中セルのテキスト
    text = Selection(1)
    
    ' テキストにABCが含まれない場合にメッセージを表示して中断する
    If Not text Like "*ABC*" Then
        MsgBox "ABCを含むセルを選択してください", vbOKOnly, "ABCセルの選択"
        Exit Sub
    End If
```

# テキストの分割

Split によりテキストを分割できます。

- 参考：[Split 関数](https://docs.microsoft.com/ja-jp/office/vba/language/reference/user-interface-help/split-function)

```vb
    ' 複数セルをコピーした後に実行すると、イミディエイトウインドウに各セルのテキストを表示する
    Dim text As String  ' クリップボードから取得したテキスト
    Dim rows As Variant  ' 行配列
    Dim row As Variant  ' 行
    Dim items As Variant  ' 項目配列
    Dim item As Variant  ' 項目
    
    text = GetTextFromClipboard()
    rows = Split(text, vbCrLf)
    If UBound(rows) = -1 Then
        Debug.Print "改行が含まれていない"
    End If
    
    For Each row In rows
        items = Split(row, vbTab)
        If UBound(items) = -1 Then
            Debug.Print "タブが含まれていない"
        End If
        For Each item In items
            Debug.Print item
        Next
    Next
```

# 図形の操作

Shape オブジェクトにより図形を取り扱うことができます。

- 参考：[Shape オブジェクト](https://docs.microsoft.com/ja-jp/office/vba/api/excel.shape)

```vb
    ' 選択中のシートに含まれる各図形に対して繰り返す
    Dim shp As Shape  ' 図形
    For Each shp In ActiveSheet.Shapes
        ' 図形の種類を表示する
        Debug.Print shp.AutoShapeType

        ' 図形の左側座標と幅を表示する
        Debug.Print shp.Left, shp.Width

        ' 図形にテキストがあれば表示する
        If shp.TextFrame2.HasText Then
            Debug.Print shp.TextFrame2.TextRange
        End If
    Next
```

# リストの活用

Collection によるリストを用いて、テキストを保持したり、区切り文字で結合できます。

- 参考：[How to join a collection in VBA](https://stackoverflow.com/questions/29015444/how-to-join-a-collection-in-vba)
- 参考：[Collection オブジェクト](https://docs.microsoft.com/ja-jp/office/vba/language/reference/user-interface-help/collection-object)
- 参考：[IIf 関数](https://docs.microsoft.com/ja-jp/office/vba/language/reference/user-interface-help/iif-function)

```vb
Private Function JoinList(target As Object, delim As String)
    ' リスト項目のテキストを結合する
    '
    ' Args:
    '   - target (Object): 対象リスト＝｛テキスト｝
    '   - delim (String): 区切り文字

    Dim res As String  ' 結合結果
    Dim text As Variant  ' テキスト
    For Each text In target
        res = IIf(res = "", text, res & delim & text)
    Next
    JoinList = res
End Function

Public Sub 動作確認()
    ' 各セルのテキストをカンマ区切りで結合して表示する
    Dim cell As Variant  ' セル
    Dim itemList As New Collection  ' 項目リスト    
    For Each cell In Selection
        itemList.Add cell
    Next    
    Debug.Print JoinList(itemList)
End Sub
```

# 辞書の活用

Dictionary オブジェクトによる辞書を用いることで、Switch 文を置き換えてスマートに実装できます。

- 参考：[Dictionary オブジェクト](https://docs.microsoft.com/ja-jp/office/vba/language/reference/user-interface-help/dictionary-object)

```vb
Public Sub 動作確認()
    ' 対象図形の種類を指定する
    Dim targetTypes As Object  ' 対象図形タイプ
    Set targetTypes = CreateObject("Scripting.Dictionary")
    targetTypes.Add msoShapeFlowchartDocument, "ドキュメントを表す図形"
    targetTypes.Add msoShapeFlowchartMultidocument, "複数のドキュメントを表す図形"

    ' 選択中のシートに含まれる対象図形のテキストを表示する
    Dim shp As Shape  ' 図形
    For Each shp In ActiveSheet.Shapes
        ' 対象図形のみを処理する 
        If Not targetTypes.Exists(shp.AutoShapeType) Then Goto Next_shp
        ' 図形にテキストがあれば表示する
        If shp.TextFrame2.HasText Then
            Debug.Print shp.TextFrame2.TextRange
        End If
Next_shp:
    Next
End Sub
```

# 辞書のソート

VBA にソート処理は用意されていないため、サブルーチンを実装する必要があります。シンプルなコードになるため、選択ソートアルゴリズムがお勧めです。

- 参考：[Selection sort](https://www.vbforums.com/showthread.php?473677-VB6-Sorting-algorithms-(sort-array-sorting-arrays)&p=2909544#post2909544)

```vb
Private Sub Sort(ByRef pvarArray As Variant)
    ' 配列をソートする
    '
    ' Args:
    '   - pvarArray (Variant): ソート対象の配列
    
    Dim i As Long
    Dim j As Long
    Dim iMin As Long
    Dim iMax As Long
    Dim varSwap As Variant
 
    iMin = LBound(pvarArray)
    iMax = UBound(pvarArray)
    For i = iMin To iMax - 1
        iMin = i
        For j = (i + 1) To iMax
            If pvarArray(j) < pvarArray(iMin) Then iMin = j
        Next
        varSwap = pvarArray(i)
        pvarArray(i) = pvarArray(iMin)
        pvarArray(iMin) = varSwap
    Next
End Sub

Public Sub 動作確認()
    ' 対象図形の種類を指定する
    Dim targetTypes As Object  ' 対象図形タイプ
    Set targetTypes = CreateObject("Scripting.Dictionary")
    targetTypes.Add msoShapeFlowchartDocument, "ドキュメントを表す図形"
    targetTypes.Add msoShapeFlowchartMultidocument, "複数のドキュメントを表す図形"

    ' 対象図形の値順にソートして表示する
    Dim indexList As Variant
    indexList = targetTypes.keys
    Sort indexList
    For Each index In indexList
        Debug.Print index, targetTypes.Item(index)
    Next
End Sub
```

# テキストを含むセルの検索

Find, FindNext によりセルを検索できます。

- 参照：[Range.Find メソッド](https://docs.microsoft.com/ja-jp/office/vba/api/excel.range.find)
- 参照：[Range.FindNext メソッド](https://docs.microsoft.com/ja-jp/office/vba/api/excel.range.findnext)

```vb
Private Function FindText(target As Range, text As String, Optional LookAt As Variant = xlWhole) As Collection
    ' 対象範囲からテキストを含むセルを検索する
    '
    ' Args:
    '   - target (Range): 対象範囲
    '   - text (String): テキスト
    '   - LookAt (Variant): 検索条件＝完全一致(xlWhole)｜部分一致(xlPart)、省略時は 完全一致(xlWhole)
    '
    ' Returns:
    '   - foundList (Collection): 検索結果リスト＝｛検索結果｝

    Dim firstAddress As String  ' 最初の検索結果のアドレス
    Dim found As Range  ' 検索結果
    Dim foundList As New Collection  ' 検索結果リスト
    
    ' 対象範囲からテキストを含むセルを検索する
    Set found = target.Find(text, LookIn:=xlValues, LookAt:=LookAt)
    If Not found Is Nothing Then
        firstAddress = found.Address
        Do
            foundList.Add found
            Set found = target.FindNext(found)
        Loop While Not found Is Nothing And found.Address <> firstAddress
    End If
    
    ' 検索結果リストを返す
    Set FindText = foundList
End Function

Public Sub 動作確認()
    ' C列からテキストを検索してアドレスを表示する
    Dim found As Variant  ' 検索結果
    For Each found In FindText(Range("C:C"), "テキスト")
        Debug.Print found.Address
    Next
End Sub
```

# 合計の計算

WorksheetFunction.Sum により指定範囲の合計を計算できます。

- 参考：[WorksheetFunction.Sum メソッド](https://docs.microsoft.com/ja-jp/office/vba/api/excel.worksheetfunction.sum)

```vb
    Debug.Print WorksheetFunction.Sum(Range("H6:I6"))
````

# ファイルの選択

Application.GetOpenFilename によりファイルを選択できます。

- 参考：[Application.GetOpenFilename メソッド](https://docs.microsoft.com/ja-jp/office/vba/api/excel.application.getopenfilename)

```vb
    ' ファイルを選択する
    Dim path As String  ' パス名
    path = Application.GetOpenFilename(Title:="ファイルを選択")
    If path = "False" Then
        Exit Sub  ' 未選択のため中断
    End If
```

# フォルダの選択

Application.FileDialog によりフォルダを選択できます。

- 参考：[Application.FileDialog プロパティ](https://docs.microsoft.com/ja-jp/office/vba/api/excel.application.filedialog)

```vb
' この変数定義はモジュールファイルの先頭（他の関数定義より前）に記述しないと正しく認識されない
Private 出力先を選択_LastOutdir As String  ' 前回選択した出力先ディレクトリ

Public Sub 出力先を選択()
    Dim outdir As String  ' 出力先ディレクトリ

    ' 出力先ディレクトリを選択する
    ' もし前回選択していればその結果を用いる
    outdir = 出力先を選択_LastOutdir
    If outdir = "" Then
        With Application.FileDialog(msoFileDialogFolderPicker)
            .Title = "出力先ディレクトリを選択"
            If .Show = False Then
                Exit Sub  ' 未選択のため中断
            End If
            outdir = .SelectedItems(1)
        End With
    Else
        出力先を選択_LastOutdir = outdir
    End If
End Sub
```

# パス名の加工

Dir により存在するファイルのファイル名部分のみを抽出できます。

- 参考：[Dir 関数](https://docs.microsoft.com/ja-jp/office/vba/language/reference/user-interface-help/dir-function)

```vb
    ' tmpl に含まれる "-テンプレート" の部分を model に置き換えて、
    ' ディレクトリを outdir に置き換える
    path = outdir & "\" & Replace(Dir(tmpl), "-テンプレート", "-" & model)
```

# リンクの作成

ActiveSheet.Hyperlinks.Add により、選択中のシートにリンクを作成できます。

- 参考：[Hyperlinks.Add メソッド](https://docs.microsoft.com/ja-jp/office/vba/api/excel.hyperlinks.add)

```vb
    ' 選択中のセルに path へのリンクを追加して「●」を表示する
    ActiveSheet.Hyperlinks.Add Anchor:=Selection(1), Address:=path, TextToDisplay:="●"
```

# リンクの取得

Hyperlinks オブジェクトからリンクの情報を参照して取得できます。

- 参考：[Hyperlink オブジェクト](https://docs.microsoft.com/ja-jp/office/vba/api/excel.hyperlinks)

```vb
    ' 選択セルにリンクがあれば表示する
    If Selection(1).Hyperlinks.Count <> 0 Then
        Debug.Print ActiveWorkbook.path & "\" & Selection(1).Hyperlinks(1).Address
    End If
```

# コメントの作成

Range.AddComment によりコメントを作成できます。

- 参考：[Range.AddComment メソッド](https://docs.microsoft.com/ja-jp/office/vba/api/excel.range.addcomment)

```vb
    ' 選択セルにコメントを追加する
    With Selection(0)
        .AddComment "コメント"
        With .Comment.Shape
            .TextFrame.AutoSize = True
            .Height = Application.Max(.Height * .Width / 200, 50)
            .Width = 200
        End With
    End With
```

# セル背景色の変更

ColorIndex プロパティによりセルの背景色を変更できます。

- 参考:[【Excel VBA】ColorIndexプロパティの設定値　インデックス番号と色の対応](https://officek.net/excelvba/v-range/vr-format/vrf-colorindexnum/)

```vb
    Selection(1).Interior.ColorIndex = 0  ' クリア(0)
    Selection(2).Interior.ColorIndex = 44  ' ゴールド(44)
```

# ブックの作成

Workbooks.Add によりブックを作成できます。

- 参考：[Workbooks.Add メソッド](https://docs.microsoft.com/ja-jp/office/vba/api/excel.workbooks.add)

```vb
    ' テンプレート tmpl から新たにブックを作成して、加工し、path に保存する
    With Workbooks.Add(tmpl)
        With .Worksheets("シート名")
            .Range("E3") = "E3"
            .Range("E4") = "E4"
        End With
        .SaveAs path
    End With
```

# ブックの参照

Workbooks.Open により既存のブックを開くことができます。

- 参考：[Workbooks.Open メソッド](https://docs.microsoft.com/ja-jp/office/vba/api/excel.workbooks.open)

```vb
    ' 既存ブックを表示せずに開き、対象セルのテキストを取得する
    Application.ScreenUpdating = False
    With Workbooks.Open("既存ブックのパス名", ReadOnly:=True)
        Windows(.Name).Visible = False
        With .Worksheets("シート名")
            Debug.Print .Range("E3")
        End With
        .Close SaveChanges:=False
    End With
    Application.ScreenUpdating = True
```

# 繰り返しのスキップ

Goto を活用して他言語における continue 相当の処理ができます。

- 参考：[Goto ステートメント](https://docs.microsoft.com/ja-jp/office/vba/language/reference/user-interface-help/goto-statement)

```vb
    ' 選択範囲の各セルにおいて、リンクが設定されていれば処理をする
    Dim cell As Variant
    For Each cell In Selection
        ' リンクが設定されていなければ無視する
        ' VBAには continue がないので Goto で代用することで、インデントを下げている
        If cell.Hyperlinks.Count = 0 Then GoTo Next_cell

        ' リンクに対して処理する
        Debug.Print cell.Hyperlinks(1).Address

Next_cell:
    Next
```

# 無限ループの中断

Alt + ESC キーを連続入力することで、無限ループを停止できます。

- 参考：[vba【無限ループの止め方】](https://qiita.com/HiroshiAkutsu/items/9ef35168757fb17277c1)