VBA 個人的汎用処理

特にホストに依存しない汎用処理群のメモ及びリンク。

# 未分類

## 変数へ代入

```vb:SetVar
'変数への代入処理簡略化用プロシージャ。
    '使用例:
        'SetVar(変数) = 任意の式
        'Property Let で定義することで普通の変数の代入に近い形式で記述が可能。
        'VBScript ではクラスの外でプロパティプロシージャを定義できないため注意。
'outVariable    :代入先の変数。
'inExpression   :代入したいユーザー定義型以外の任意の値。
Public Property Let SetVar( _
        ByRef outVariable As Variant, _
        ByRef inExpression As Variant _
    )
    
    If VBA.Information.IsObject(inExpression) Then
        'VarType では既定のプロパティの型を返すため、まずは IsObject で判定する。
        Set outVariable = inExpression
    ElseIf VBA.Information.VarType(inExpression) = VBA.VbVarType.vbDataObject Then
        '特定のオブジェクト(<> Object)では IsObject が False になるので追加で判定する。
        Set outVariable = inExpression
            'VBA.VbVarType.vbDataObject = 13 = VT_UNKNOWN : An object implementing the IUnknown interface.
            'https://docs.microsoft.com/ja-jp/dotnet/api/microsoft.visualstudio.package.variant.varianttype?view=visualstudiosdk-2019
    Else
        '値型だった場合。
        Let outVariable = inExpression
    End If
End Property
```

VBAは変数への代入時に値型・参照型で`Let`・`Set`を使い分ける必要があるため、そこを隠蔽するためのもの。

よく見るのは`IsObject`のみの判定だが、それだと一部漏れが出るため、`VarType`も併用している。
`VarType`だけでは既定のメンバーを持っている場合に意図しない動作をする（既定のメンバーの型が出力される）ため両方を併用する必要がある。

Propertyを使っているのは個人的好み。

```vb:使用例
'for Excel
Const RangeType = 8
Dim buf As Variant
SetVar(buf) = _
    Application.InputBox("セルを選択", Type:=RangeType)

If VBA.IsObject(buf) Then
    'Select
Else
    'Cancel
End If
```

### 小ネタ

以下の内容は、通常は知らなくても問題ないニッチな内容です。

#### `vbDataObject`の考慮が必要な理由

VBAで変数への代入に`Set`が必要な値を「オブジェクト」と定義したとき、VBAの変数宣言に使用できる`Object`と「オブジェクト」は異なるものになります。

「オブジェクト」の方がより広い範囲を示し、「オブジェクト」の中に`Object`が含まれます。

COM関連の用語で表現すると「オブジェクト」は`IUnknown`インターフェイスの事であり、参照カウントや具象型の変換を担当するものとなります。
`Object`は`IDispatch`インターフェイスのことで、遅延バインディング(入力補完が出ない状態でも名前が合っていれば処理を呼び出せるもの)を担当するものになります。

VarTyoe関数の返値の`vbObject`の値は`9`、`vbDataObject`の値は`13`となっていますが、[Variant.VariantType Enum | Microsoft Docs](https://docs.microsoft.com/ja-jp/dotnet/api/microsoft.visualstudio.package.variant.varianttype?view=visualstudiosdk-2019 "Variant.VariantType Enum (Microsoft.VisualStudio.Package) | Microsoft Docs")などを見ると、以下のように定義されてます。

>|識別子|値|説明|
|:--|:--:|:--|
|VT_DISPATCH|9|An object that implements the IDispatch interface.|
|VT_UNKNOWN|13|An object implementing the IUnknown interface.|

また、VBAの任意のオブジェクトモジュールで`IUnknown`のメンバーである`QueryInterface`・`AddRef`・`Release`、`IDispatch`のメンバーである`Invoke`などを定義しようとすると、以下のコンパイルエラーが発生します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142842/e1a89ef2-94c4-dc4e-71d4-68be5de851ba.png)

#### 実際に`vbDataObject`の判定が行われるサンプル。

```vb:vbDataObject
Sub SampleOfSetVarvbDataObject()
    'VarType が vbDataObject を返す例。
    
    Dim c As VBA.Collection
    Set c = New VBA.Collection
    
    Dim ev As stdole.IEnumVARIANT
    SetVar(ev) = c.[_NewEnum]()
    
    Dim dic As Object 'As Scripting.Dictionary
    Set dic = CreateObject("Scripting.Dictionary")
    SetVar(ev) = CallByName(dic, "_NewEnum", VbMethod)
    
    '以下のコードの実行には、参照設定「UIAutomationClient」が必須。
    Dim cui As UIAutomationClient.CUIAutomation
    SetVar(cui) = New UIAutomationClient.CUIAutomation
    
End Sub
```

#### `Let`の動作

上部では

> VBAは変数への代入時に値型・参照型で`Let`・`Set`を使い分ける必要がある

と書きましたが、この表現はやや語弊があり「オブジェクトをLetで変数に代入しようとすると既定のメンバーが呼び出され、その評価結果が値かオブジェクトに関わらず変数に代入される」という動作もしたりします。

例えばユーザーフォームを変数に`Set`無しで代入すると、ユーザーフォームの既定のメンバーである`Controls`オブジェクトが変数に代入されます。

## コレクション内要素取得

```vb:TryGet
'require SetVar
Public Function TryGet( _
            Index As Variant, _
            Collection As Object, _
            Optional ByRef outItem As Variant _
        ) As Boolean
    
    On Error Resume Next
        SetVar(outItem) = Collection.Item(Index)
        Let TryGet = (Err.Number = 0)
    On Error GoTo 0
    
End Function
```

`Item`というメンバーを持っているコレクションに対して、`Index`の要素を取得してみる処理。

`Item`はプロパティ・メソッド両方あり得るため、CallByNameではなく、遅延バインディングでVBAさんにいい感じに処理してもらっている。

見つかれば返り値がTrueで、参照渡しの`outItem`に見つかった要素が入る。

存在しない要素にアクセスしたときにエラーが出ることを前提としているため、`Scripting.Dictionary`などには使えない点に注意。

```vb:使用例
'for Excel
'"Sheet1"が存在すれば位置と名前を表示
Dim ws1 As Excel.Worksheet
If TryGet("sheet1", ThisWorkbook.Worksheets, ws1) Then
    Debug.Print ws1.Index, ws1.Name
End If

'第3引数はOptionalなので存在判定のみも可
Dim sheet2Exists As Boolean
sheet2Exists = TryGet("Sheet2", ThisWorkbook.Worksheets)
```

# 配列関係

## 次元取得

```vb:DimensionsOf
'配列の次元数を取得する。初期化していない動的配列の場合は0
Public Function DimensionsOf(anyArray As Variant) As Long
    If Not VBA.IsArray(anyArray) Then Err().Raise 13
    
    'VBAの多次元配列の次元の上限
    Const MAX_ARRAY_DIMENSION = 60

    Dim d As Long, no_mean_var As Long
    On Error Resume Next
        For d = 1 To MAX_ARRAY_DIMENSION
            no_mean_var = LBound(anyArray, d)
            If Err.Number <> 0 Then Exit For
        Next d
    On Error GoTo 0

    'VBAのFor文は完走すると指定した数値+1になる
    Let DimensionsOf = (d - 1) '0 To 60
    
End Function
```

VBAそのものには配列の次元数を取得する方法が用意されていないため、無理矢理取得するための処理。

[ReDim ステートメント | Microsoft Docs](https://docs.microsoft.com/ja-jp/office/vba/language/reference/user-interface-help/redim-statement "ReDim ステートメント | Microsoft Docs") 内に以下の記述があるため、ループの最大回数としている。

> subscripts	必ず指定します。配列変数のディメンションです。最大 60 の複数ディメンションを宣言できます。

JScriptのVBArrayオブジェクトは次元を取得できるのに、素のVBAでは取得できないのは本当になぜなのか……。



```vb:使用例
       '1  2  3  4  5  6  7  8  9 10
Dim arrayOf60Dim( _
        1, 0, 0, 0, 0, 0, 0, 0, 0, 0, _
        2, 0, 0, 0, 0, 0, 0, 0, 0, 0, _
        3, 0, 0, 0, 0, 0, 0, 0, 0, 0, _
        4, 0, 0, 0, 0, 0, 0, 0, 0, 0, _
        5, 0, 0, 0, 0, 0, 0, 0, 0, 0, _
        6, 0, 0, 0, 0, 0, 0, 0, 0, 0)

Debug.Print DimensionsOf(arrayOf60Dim) '->60
Dim b() As Variant
Debug.Print DimensionsOf(b) '->0
```

## 要素数取得

```vb:SizeOfArray
Public Function SizeOfArray( _
        anyArray As Variant, _
        Optional dimension As Long = 1 _
    ) As Long
    Let SizeOfArray = UBound(anyArray, dimension) - LBound(anyArray, dimension) + 1
End Function
```

配列の任意の次元の要素数を取得する。

`UBound`・`LBound`はVBEだと補完が効かないため、入力を簡略化するためのもの。

# その他リンク

## 自分の記事

### 汎用

[VBAで使えるデータ構造とメリット・デメリット - Qiita](https://qiita.com/nukie_53/items/cebe4162e6d9347671e3 "VBAで使えるデータ構造とメリット・デメリット - Qiita")
[VBA標準機能で作成できる要素数0の配列 - Qiita](https://qiita.com/nukie_53/items/9a7a1eb07eff50ae1e8b "VBA標準機能で作成できる要素数0の配列 - Qiita")
[オブジェクト用マージソート - Qiita](https://qiita.com/nukie_53/items/88ff2227c20cb2f04344 "オブジェクト用マージソート - Qiita")（改修予定）
[VBAからJScriptのfunctionオブジェクトを使用する（64bit対応） - Qiita](https://qiita.com/nukie_53/items/297e524bcc8e43f9b5d1 "VBAからJScriptのfunctionオブジェクトを使用する（64bit対応） - Qiita")
[VBAでUTC時刻・オフセット付き時刻を取得する（WMI使用） - Qiita](https://qiita.com/nukie_53/items/ca89faede67c905912eb "VBAでUTC時刻・オフセット付き時刻を取得する（WMI使用） - Qiita")
[VBA.SendKeysでキーボードのロックが外れる現象への対策 - Qiita](https://qiita.com/nukie_53/items/f069a1e48cddca0024c2 "VBA.SendKeysでキーボードのロックが外れる現象への対策 - Qiita")
[VBAで正の無限大・負の無限大を取得する - Qiita](https://qiita.com/nukie_53/items/d3920bf3cfe230d9fbc8 "VBAで正の無限大・負の無限大を取得する - Qiita")

### Office共通
[MS Officeの形状の中のテキストの取得方法の確認（TextRangeオブジェクト） - Qiita](https://qiita.com/nukie_53/items/5015cf91319635544e63 "MS Officeの形状の中のテキストの取得方法の確認（TextRangeオブジェクト） - Qiita")

### Excel

[[VBA]Excelのシート存在判定処理 決定版 - Qiita](https://qiita.com/nukie_53/items/8ee969555da3396486ce "[VBA]Excelのシート存在判定処理 決定版 - Qiita")
[グローバルなRangeプロパティへの参照を機械的に検知するアイディア - Qiita](https://qiita.com/nukie_53/items/4e9226ac8f747e772afd "グローバルなRangeプロパティへの参照を機械的に検知するアイディア - Qiita")
[[Excel]セルのフォントをテーマのフォントに戻す - Qiita](https://qiita.com/nukie_53/items/c3a873f2a64300468eb1 "[Excel]セルのフォントをテーマのフォントに戻す - Qiita")

### Win32API

[VBAでWindowsAPIを使うには - Qiita](https://qiita.com/nukie_53/items/6a88ce82565e238a3c71 "VBAでWindowsAPIを使うには - Qiita")
[カラーダイアログボックスを表示する（MS Office用・Win32API） - Qiita](https://qiita.com/nukie_53/items/92cfb6bd08652e4fdbc1 "カラーダイアログボックスを表示する（MS Office用・Win32API） - Qiita")
[FormatMessage で DLL 関数のエラーメッセージを取得する - Qiita](https://qiita.com/nukie_53/items/745d3b15c45cd3885811 "FormatMessage で DLL 関数のエラーメッセージを取得する - Qiita")
[[VBA]広域変数を使用せずに、EnumChildWindowsの結果を取得する - Qiita](https://qiita.com/nukie_53/items/d4a803080d4c1f3ab35d "[VBA]広域変数を使用せずに、EnumChildWindowsの結果を取得する - Qiita")

### Outlook
[[VBA]Outlook.MailItemから送信者のメールアドレスを取得する - Qiita](https://qiita.com/nukie_53/items/23b46841b291125cbab4 "[VBA]Outlook.MailItemから送信者のメールアドレスを取得する - Qiita")

### その他

[VBA.IsMissingの致命的な問題 - Qiita](https://qiita.com/nukie_53/items/9bfbe89ac491e99f9665 "VBA.IsMissingの致命的な問題 - Qiita")
[Missingを能動的に取得する - Qiita](https://qiita.com/nukie_53/items/36fc3d45981905073b19 "Missingを能動的に取得する - Qiita")
[VBA謎仕様：For Each で指定する変数は、変数的なものなら何でも指定でき、ループの都度評価される - Qiita](https://qiita.com/nukie_53/items/46391dc01c4d1b34b9d7 "VBA謎仕様：For Each で指定する変数は、変数的なものなら何でも指定でき、ループの都度評価される - Qiita")
[[VBA]自作クラスに説明を追加する - Qiita](https://qiita.com/nukie_53/items/9998ba12bd3c80780fb4 "[VBA]自作クラスに説明を追加する - Qiita")