特にホストに依存しない汎用処理群のメモ及びリンク。
未分類
変数へ代入
'変数への代入処理簡略化用プロシージャ。
'使用例:
'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を使っているのは個人的好み。
'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などを見ると、以下のように定義されてます。
|識別子|値|説明|
|:--|:--:|:--|
|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
などを定義しようとすると、以下のコンパイルエラーが発生します。
実際に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
オブジェクトが変数に代入されます。
コレクション内要素取得
'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
などには使えない点に注意。
'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)
配列関係
次元取得
'配列の次元数を取得する。初期化していない動的配列の場合は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 内に以下の記述があるため、ループの最大回数としている。
subscripts 必ず指定します。配列変数のディメンションです。最大 60 の複数ディメンションを宣言できます。
JScriptのVBArrayオブジェクトは次元を取得できるのに、素のVBAでは取得できないのは本当になぜなのか……。
'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
要素数取得
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
VBA標準機能で作成できる要素数0の配列 - Qiita
オブジェクト用マージソート - Qiita(改修予定)
VBAからJScriptのfunctionオブジェクトを使用する(64bit対応) - Qiita
VBAでUTC時刻・オフセット付き時刻を取得する(WMI使用) - Qiita
VBA.SendKeysでキーボードのロックが外れる現象への対策 - Qiita
VBAで正の無限大・負の無限大を取得する - Qiita
Office共通
MS Officeの形状の中のテキストの取得方法の確認(TextRangeオブジェクト) - Qiita
Excel
[VBA]Excelのシート存在判定処理 決定版 - Qiita
グローバルなRangeプロパティへの参照を機械的に検知するアイディア - Qiita
[Excel]セルのフォントをテーマのフォントに戻す - Qiita
Win32API
VBAでWindowsAPIを使うには - Qiita
カラーダイアログボックスを表示する(MS Office用・Win32API) - Qiita
FormatMessage で DLL 関数のエラーメッセージを取得する - Qiita
[VBA]広域変数を使用せずに、EnumChildWindowsの結果を取得する - Qiita
Outlook
[VBA]Outlook.MailItemから送信者のメールアドレスを取得する - Qiita
その他
VBA.IsMissingの致命的な問題 - Qiita
Missingを能動的に取得する - Qiita
VBA謎仕様:For Each で指定する変数は、変数的なものなら何でも指定でき、ループの都度評価される - Qiita
[VBA]自作クラスに説明を追加する - Qiita