VBA/VBSはじめの一歩


以下は個人的に必要な事柄をまとめただけなので、網羅性とかまったくないです。

# そもそもVBAとは

Visual Basic for Application、要はVisual BasicのOffice実装版。「マクロ」と呼ばれる自動処理系はVBAで記述されている。似た言葉でVBSというのもあるが、こちらはVBScript、Visual Basic風のWindows用スクリプト言語で別のもの。VBAがVisual Basicそのものであるのに対し、VBScriptはあくまでVB **風** で仕様は異なる。本記事内では両者を区別せずに扱っているが、厳密には実装の異なる部分があるはず（雑ですみません）

その他、VBS/VBA周りの用語に関して。

## WSH (Windows Script Host)

Windows上でのスクリプト実行環境。言語自体ではなく、実行環境を指す名称であり、WSH上で実行可能な言語はVBScriptとJScriptがある。現状はその役割はPowerShellに譲られている。

## WMI (Windows Management Instrumentation)

WSH上で操作可能な、Windowsのシステム情報を制御するCOMオブジェクト。個人的にVBSを書くときはWMIを操作する目的であることが多い。

## WQL (SQL for WMI)

WMIからの情報取得用に拡張されたSQLの実装。VBScriptから実行することができるので、WMIを操作する場合はよく用いる。

# さっそく文法

## 変数／定数
* `Dim foo As String` 変数宣言
* `Const Bar As Long` 定数宣言

変数は宣言しなくても使えるが、やはり宣言した方がよい。先頭で`Option Explicit`と記述しておくと変数の宣言が必須になるので、厳密な記法を望む場合は記述を推奨。

変数の型は[ここ](https://msdn.microsoft.com/ja-jp/library/47zceaw7(v=vs.120).aspx)に一覧されている。特徴的なものとして「オブジェクト型」があり、これは文字通りオブジェクト指向のそれであり、プロパティやメソッドを持っている。。例えばExcelのVBAを書く場合はWorksheetやCellといったオブジェクトが用意されており、Cellにはrowやcolumnといったプロパティ、deleteやsplitといったメソッドが用意されているといった具合。

オブジェクト型変数は扱い方が他の型と異なる部分があるため注意が必要。代入は他の型では`=`を用いるが、オブジェクト型変数に関しては`Set`を頭に付与しなくてはならない。またオブジェクト型変数は参照代入になる点も注意が必要。したがって望むらくは、終了時点で`Nothing`を代入して解放するのが良い。

```vbnet
Dim dataVar As Integer
dataVar = 100
Dim objVar As Object
Set objVar = Range("A1")
Set objVar = Nothing
```

`Set`がなければ当然エラーだし、宣言されたオブジェクト型変数が必要なところで別のデータ型の変数を持ってくると受け付けてくれないなど、それなりに型については厳格（だがエラーメッセージが要領得なくて気付きにくい）。上記のObject型はあらゆるオブジェクト型を含むことができたり、さらに動的データ型であるVariant型なんてのもあるが、良識あるエンジニアであれば使用は控え、strictな型宣言をすべきかと。

## 演算子
だいたいは想像した通りのものだが、いくつか注意点がある。

* 除算は`/`だが、余りを除いた商を求める場合は`\`を使う。剰余を求めるには`mod`演算子というのはだいぶいただけない。。。
* 代入も比較演算にも`=`を使うというのもぐぬぬ感ある。
* 等価比較はオブジェクト型変数だと`Is`を使い、データ型変数だと`=`を使う。

## 配列と動的配列
配列は変数名の末尾に(n)を付加することで、要素数n+1（添字が0からカウントされるため）の配列として宣言できる。

```vbnet
Dim foo(2) As Integer
foo(1) = 12
foo(2) = 23
Dim bar(3) As String
bar = Array("a", "b", "c")
```

要素数が動的に変化する場合は、nを抜いて`foo()`の形で定義することも可能ではあるが、本来的には動的配列であるCollectionオブジェクトを用いるのが望ましい。なお、Collectionオブジェクトは各要素のデータ型を問わない。

```vbnet
Dim col As Collection
col.Add(2)
col.Add("テスト")

col.Count ' => 2
col.Item(1) ' => テスト
```

## 関数
`Sub`で宣言する。スコープとして`Public`、`Private`を宣言でき、省略した場合は`Public`として扱われる。要はここで宣言した関数名がマクロ名になる。

```vbnet
Public Sub Sample()
End Sub
```

## 制御式
### if
```vbnet
If foo > 0 Then
  MsgBox "fooは正です"
Else
  MsgBox "fooは0以下です"
End If
```
### Loop
```vbnet
For i = 0 To Len(str)
  MsgBox Mid(str, 1, i)
Next i ' => str変数の文字を1文字ずつ表示

For Each i In array
  MsgBox i
Next i ' => array配列の要素を1つずつ表示

i = 0
Do While True
  If i = 10 Then
    Exit Do ' => break
  End If
  i = i + 1
Loop
```

## 文字列操作
```vbnet
Dim foo As String
Dim bar As String

foo = "あいう"
bar = "ABC"
MsgBox foo & bar ' => あいうABC
baz = Replace(foo, "あい", "かき") ' => かきう
baz = Left(foo, 2) ' => かき
baz = Right(foo, 2) ' => きう
qux = InStr(foo, "き") ' => 2
qux = InStr(foo, "い") ' => 0

array = Split("aaa,bbb,ccc", ",")
For Each i In array
  MsgBox i
Next i ' => aaa, bbb, cccの順にダイアログ表示
```

ちなみにこれは文字列操作に限った話ではないが、非常に長い文字列を代入する場合など、1文が長くなる際には`_`（アンダースコア）を使って式を改行できる。

```vbnet
LongMsg = "非常に長いメッセージを代入する場合には、" & vbCrLf & _
          "このように途中で改行することが可能です。"
```

## 日付と時刻
```vbnet
Dim d As Date
Dim t As Time

d = Date ' 実行時点の日付を取得
t = Time ' 実行時点の時刻を取得
MsgBox "今日は" & Format(d, "yyyy年mm月dd日") & "です" ' => 今日は2015年08月11日です
d = DateAdd("d", -2, d) ' 2日前の日付を取得
MsgBox "2日前は" & Format(d, "mm/dd") & "です" ' => 2日前は08/09です
t = DateAdd("n", 20, t) ' 20分後の時刻を取得（月との重複避けたとは言えnはねーわ）
MsgBox "20分後は" & Format(t, "h時n分") & "です" ' => 20分後は0時20分です
```

## 正規表現
RegExpオブジェクトを使用して検索や置換ができる。検索は`Test`メソッドでは真偽値を返すのみにとどまるが、`Execute`を使うとマッチした文字列をMatchesコレクションに返してくれる。括弧を用いれば後方参照が可能。

```vbnet
Dim re As Object
Dim target As String
Dim matches As Object

Set re = CreateObject("VBScript.RegExp")
With re
  .Pattern = "([a-z]+)([0-9]+)"
  .IgnoreCase = False
  .Global = True
End With

target = "abc123"
If re.Test(target) Then
  Set matches = re.Execute(target)
  MsgBox matches.Item(0).SubMatches(1) ' => 123
End If
```

## ファイルの取り扱い
Excel VBAでセルやワークシートを扱うことが多いだろうが、ここでは一旦一般的なファイルの読み書きの話にとどめる。

ファイルをオープンする場合はパスの他に「ファイル番号」が必要となる。基本的にはその時点で使われていないファイル番号を`FreeFile`関数で取得すれば問題ない。

ファイルへの書き出しは下記`Print`の他に`Write`が使えるが、`Write`では出力文字列がダブルクォーテーションで囲まれてしまうという点、`Print`の方が[オプションが豊富である点](https://msdn.microsoft.com/ja-jp/library/office/gg264278.aspx)等を考えると、`Print`を使うのが無難。

```vbnet
fileNo = FreeFile
filePath = "c:\example.txt"

Open filePath For Output As #fileNo
Print #fileNo, "テスト"
Close #fileNo
```

## 連想配列
Dictionaryオブジェクトを使用する。連想配列として必要になる機会はあまりないのだが、重複要素の排除が配列では難しいので、こちらを使って実装したりしている。

```vbnet
Set dic = Create.Object("Scripting.Dictionary")

dic.Add key, value
If Not dic.Exists(value) Then
  dic.Add key, value
End If
```

## 環境変数
関数を用いて`Environ("HOME")`の形で参照できる。環境変数の登録順序を表す整数も引数に取れるらしいが、そんなものを使うのはループで全環境変数を取り出すときぐらいな気がする。

# 注意点
* 実行時に出るエラーメッセージをそのまま読み解くのは諦めた方がいい。エラーが出たらF8でステップイン実行するとか、ブレークポイントを設けるとかして該当箇所を探し、その上でメッセージをググる。
* VBAはOfficeスイートの効率化という主目的の関係上、Web上のノウハウは少し緩めのコーディング規約に従っている場合も少なくない（プログラマーのように製品開発をするわけではないので、そのこと自体が悪いとは思わない）。読みにくいけどMSDNの公式ドキュメントには当たった方がいい。