はじめに
ExcelファイルをPythonで処理しようと考えていたが、VBAで書かなければならなくなった。
頑張ってVBAでコードを書き始めた時に引っかかったところをまとめたメモです。
最初の一歩
おおよそはPythonの書き方と同じ(暴論)
VBAはブロックの先頭に :
をつけないで、ブロックの終端に処理が必要、と覚えておけばおおよそ対応できそうです。
' : は不要
Sub Test()
' メッセージボックスを表示する
MsgBox "Hello World!"
' ブロック終端。関数(Sub)の場合は End Subが必要
End Sub
様々な宣言は大文字で始まる
大文字・小文字はVBAのエディタで勝手に修正されるので、あまり気にしないでも良さそう。
変数などの大文字・小文字も自動で修正されるのはありがたい…
おまじない
Option Explicit
時々ファイルの先頭についているやつです。
これをファイル先頭に記載すると、以降は変数の宣言が必須になります。
参考: https://learn.microsoft.com/ja-jp/office/vba/language/reference/user-interface-help/option-explicit-statement
変数
型と一緒に「変数のみ」宣言する
Pythonの型ヒント付き変数 hoge: int
に近いです。
' Dim 変数名(日本語OK) As 型
Dim hoge As Long
Dim ほげ As String
' _ から始まる変数名は不可
' Dim _hoge As Long ' エラー
' 宣言時に代入は不可
' Dim hoge As Long = 42 ' エラー
' 型の指定が無いと、なんでも入れられる Variant 型になる
Dim anything ' As Variant
ただし、定数の宣言は同時に値も代入する
ややこしい…
Const FUGA As Long = 0
値の代入はオブジェクト型のみ Set
を付ける
Long
, String
のように、どのタイプの値が入るか決まっている場合は データ型
WorksheetやRangeのように、PythonのClassっぽい役割を持つのが オブジェクト型(暴論)
Dim hoge As Long
hoge = 42 ' データ型に値を入れる場合はそのまま = でつなげばよい
' オブジェクト型の代入は頭に `Set` を付ける必要がある
Dim testSheet As Worksheet
' testSheet = ThisWorkbook.Sheets("Sheet1") ' エラー
Set testSheet = ThisWorkbook.Sheets("Sheet1")
' これは許されないが
' Set hoge = 42
' これが許される時がある…
' デフォルトプロパティというらしく、プロパティを省略するとデフォルトで `Value` が選択される
' F1の値が整数だった場合にはこのコードが通ってしまう
hoge = testSheet.Range("F1") ' testSheet.Range("F1").Value と同じ
Debug.Print hoge ' イミディエイトウィンドウにhogeの値が出力される
条件式
イコールは =
ノットイコールは <>
まだ <>
はわかるが、 =
はちょっとびっくりした…
' If の末尾には Thenを付ける。 Pythonの `:` みたいなやつ
If hoge = "pen" Then
MsgBox "This is a " & hoge ' 文字列は & で結合できる
' elif ではなく ElseIf
ElseIf hoge <> "pen" Then
MsgBox "This is a not pen"
Else
MsgBox "Elseの場合はThenが不要"
' If文の終端を明示する必要がある
End If
And Or は全ての条件式を実行する
Pythonは短絡評価なので評価が不要な式は実行しないですが、
VBAは完全評価なので全ての式を実行します
' まだ宣言しただけなので、オブジェクトへの関連付けが無い状態(= Nothing)
Dim testSheet As Worksheet
' testSheet.name = "Sheet1" まで実行してしまうので、エラー
If testSheet is Nothing Or testSheet.Name = "Sheet1" Then
Debug.Print "!!!"
End If
三項演算子のことは…
IIF
というのがあるらしいが、短絡評価ではないのでPythonと動作が変わる可能性があります。
忘れましょう。
matchの代わりは存在する
Select Case
が使えます。
ただ書き方は結構違います。できることが多いと困る…
Select Case x
Case 0
Debug.Print "x = 0"
Case Is < 0
Debug.Print "x < 0"
Case 1, 2
Debug.Print "x = 1~2"
Case 3 To 10
Debug.Print "x = 3~10"
Case Else
Debug.Print "x > 10"
End Select
ループ
breakはある
Exit For
を使用します。
' VBAのForはPythonのrangeを指定したときの動作に近い
' For 初期化 To 終了条件
For i = 1 To 10
If sht.Cells(i, 1).Value = 5 Then
Exit For ' Pythonの break
End If
Debug.Print sht.Cells(i, 1).Value
Next i ' iを1つ増やす
continue はない
GoTo を使って疑似的にループを飛ばす処理がメジャーっぽいです。
あきらめてネストを1段深くした方がラベル重複による変な場所へ飛ぶ、t事故は減りそうな気がしますが…
' 要素を取りだすPythonっぽいForを使いたい場合はFor Eachを使う
For Each rng In testSheet.Range("A1:A10")
' 本当は rng.Value = "" の時にcontinueしたい…
If rng.Value <> "" Then
' セルに値があるときは処理をする
End If
Next rng ' 次の要素を取り出す
複数の値を保持する変数
listの代わり: Array か Collection
配列のインデックスは 1 始まりなので注意が必要です。 もっと複雑でした。追記にて修正。
ArrayはC言語のlistに近く、どこかのタイミングで配列数を指定しないといけません。
ReDim
で配列数を変更できるが、おそらく「要素を1個足す -> ReDimで配列を1個増やす」で疑似的に要素数不定にさせることは非推奨なはず…
' ()を付けるとArrayになる
Dim x(3) As Long
x(1) = 1
x(2) = 2
x(3) = 3
追記:
https://qiita.com/kz-muri/items/73f96908b764399f577e#comment-3fd137a8fa205b637576
https://qiita.com/kz-muri/items/73f96908b764399f577e#comment-37b5de896e8204b6ac9f
で頂いたコメントの通り、インデックスの始まりは0であったり、1であったりします。VBA難しい…
Sub test()
Dim x(5) As Long ' 末尾のインデックスを指定しているイメージ?
Debug.Print LBound(x) & ", " & UBound(x) ' 0, 5
Debug.Print VarType(x) ' 8195 = vbLong + vbArray
Dim y As Variant
y = ActiveSheet.Range("A1:A5").Value ' 5セル分の値を配列に
Debug.Print LBound(y) & ", " & UBound(y) ' 1, 5
Debug.Print VarType(y) ' 8204 = vbVariant + vbArray
Dim z(1 To 5) As Long ' Option Baseの値にとらわれずに1始まりにするならこうか…?
Debug.Print LBound(z) & ", " & UBound(z) ' 1, 5
End Sub
さらに1始まりを設定しても0から始まる配列もある。 なんだこれ!!!
Option Base 1
Sub test2()
Dim x(5) As Long ' 末尾のインデックスを指定しているイメージ?
Debug.Print LBound(x) & ", " & UBound(x) '1, 5
Debug.Print VarType(x) ' 8195 = vbLong + vbArray
Dim s As Variant
s = Split("1,2,3,4,5", ",")
Debug.Print LBound(s) & ", " & UBound(s) '0, 4
Debug.Print VarType(s) ' 8200 = vbString + vbArray
End Sub
CollectionはPythonのlist…というよりはキーを省略できるnamedtupleの方が近いかも?。
追加・削除の制約なし。ただし、中身の型は指定できません。
' Collectionの場合は最初にNewを付けるとSetしなくて良い。Asって単なる型宣言じゃないのか…?
Dim coll As New Collection
'Dim coll2 As Collection
'Set coll2 = New Collection ' これも動く。
coll.Add "hoge" ' Arrayと違い、要素を追加する場合は 変数名.Addを使う
coll.Add "fuga"
coll.Add "piyo", "p" ' キーを付ける場合は後ろに記載
Debug.Print coll(1) ' hoge Collectionのインデックスは1始まり
Debug.Print coll(3) ' piyoをインデックスで取得。
Debug.Print coll("p") ' piyoをキーで取得
dictの代わりもある
Python 3.7以降の仕様と異なり、代入した順番は保証されません。
Dim dict As Object
Set dict = CreateObject("Scripting.Dictionary")
dict.Add "x", 100 ' Collectionの時と順番が違い、キー, バリュー の順で指定
dict.Add "y", 200
Debug.Print dict("y") '200
列挙体もある
Enumをクラス経由ではなく直接定義します。
最初の数を指定してあげれば後は自動で値が入る、pythonの auto()
のような機能があって便利です。
Enum Numbers
foo = 1
bar ' 2
baz ' 3
End Enum
Sub Test()
Debug.Print Numbers.bar ' 2
End Sub
関数
Sub と Function
関数( 追記:「プロシージャ」なので関数とは違うらしい… 参考: https://learn.microsoft.com/ja-jp/office/vba/language/concepts/getting-started/calling-sub-and-function-procedures )
の定義方法はなぜか複数あります。ざっくりとした違いは以下。
- Subは戻り値が無く、ボタンに登録できる関数
- Functionは戻り値を定義できるが、ボタンには登録できない関数
Functionの戻り値用変数は その関数名である ことに注意。
Sub Test()
' 戻り値のない関数を実行するときは頭に `Call` をつける
Call TestSub(1)
Debug.Print TestFunction(1)
End Sub
Sub TestSub(x As Long)
Debug.Print x
End Sub
' 変数宣言時、戻り値の型も指定する
Function TestFunction(x As Long) As Long
' 戻り値の設定
TestFunction = x + 1
End Function
引数は基本「参照渡し」
値渡しを明示しない、 もしくは引数に ByRef
を付けると参照渡しになります。
引数に ByVal
と明示すると値渡しになります。
Sub Test()
Dim x As Long
x = 1
Call TestSub(x)
Debug.Print x ' 参照渡しのため、引数に渡した変数の値が2になっている
Debug.Print TestFunction(x) ' 戻り値は3だが
Debug.Print x ' 値渡しのため、引数に渡した変数の値は2のまま
End Sub
' 参照渡し
Sub TestSub(x As Long)
x = x + 1
End Sub
' 値渡し
Function TestFunction(ByVal x As Long) As Long
x = x + 1
TestFunction = x
End Function
エラー
おまじない
On Error GoTo ラベル名
でエラー発生時に指定したラベルまで飛びます。
err.Description
でエラーの内容を取得可能です。
Sub Test()
On Error GoTo Label
' 何らかの処理
Exit Sub ' 正常終了時はここで終了
Label:
MsgBox "エラー内容: " & err.Description
End Sub
エラーを出す
呼び出し元にエラーを投げ返したい場合は err.Raise
を使用します。
エラー番号は 513~65535がユーザー定義の範囲だが、1004などの値が既に使われているので、注意が必要です。
参考: https://excel-ubara.com/excelvba4/EXCEL242.html
いったんここまで
関数を Call する際に()を省略できたり、エラーを握りつぶして処理を続行するなどの細かい部分や、クラスに関してなどはまた別の記事で作成予定です…