このページに含まれるプログラム、ファイル等については、
Withを使うと書く手間が省け、よりすっきりしたプログラムが書けます。しかし、うっかりするととんでもない落とし穴があります。次のプログラムを見てください。A1,B1,C1にそれぞれ1,2,3と記入します。
Sub test()
ActiveSheet.Range("A1").Select
Selection.Value = 1
ActiveSheet.Range("B1").Select
Selection.Value = 2
ActiveSheet.Range("C1").Select
Selection.Value = 3
End Sub
「3行目と5行目のSelectionがWithで省略できるな…」と思った人、既にはまっています。
Sub test2()
ActiveSheet.Range("A1").Select
With Selection
.Value = 1
ActiveSheet.Range("B1").Select
.Value = 2
ActiveSheet.Range("C1").Select
End With
Selection.Value = 3
End Sub
このプログラムは上の物とは異なった動作をします。A1に1を記入した後、さらにA1に2を記入し、C1に3を記入します。つまり、Withで囲んだ時点で省略部分の対象オブジェクトを固定してしまうのです。しかも、test2でもC1に記入されることから、選択範囲は刻々と変わっていってる事がわかります。
Withは単にプログラムを見やすくするためのお約束ではなく、対象オブジェクトの指定というれっきとしたプログラムの一部であることを忘れず、むやみやたらと使わないようにしましょう。
よく言われることです。短いプログラムなら、めんどくさいでしょうが。Office VBAではOption Explicitとプログラムの頭に記述することにより、変数の宣言(定義)が義務化されます。
変数の名前の付け方にもいろいろ流儀があると思いますが、私はOffice VBAでは次のようにしてプログラミングしています。
myBorder.LineStyle = ls
のように変数lsで設定しようとしています。このとき、lsはどのように宣言したらよいでしょうか。Variantで宣言するしかないでしょうか?Dim ls As XlLineStyle
文字列(String)はC言語なんかをちょっとかじっていれば分かると思いますが、他の数字とは扱いが異なります。にもかかわらず、VBAでは「+」でつなげることが出来たりします。ここで、VBAお得意の「賢い自動変換」がかかわってくるので話がややこしくなります。次の例を見てください。
Sub aaa()
MsgBox "1" + "2" '12
MsgBox 1 + "2" '3
MsgBox "1" + 2 '3
MsgBox 1 + 2 '3
MsgBox "a" + 1 'error!
End Sub
コメントの数字が実際出てくる答えです。文字列と文字列の結合のときだけ文字列として見てくれることが分かります。しかも、最後の例では思わぬエラーが出ることになりました。どうすれば良いでしょう?
数字などを文字列として扱いたい場合は、Cstrという関数を使って、
MsgBox "a" + Cstr(1)
と明示的に文字列に変換する方法もあります。しかし、次の方法が最も簡単だと思います。
MsgBox "a" & 1
これは、「足し算じゃなくて、文字列の結合なんだよ」という記号です。見た目にも足し算と違うことが分かるので良いのではないでしょうか。
下のプログラムではキャンセルとして、Nullを返しています。この方法はUserFormのトグルボタン(三状態可能にするとTrue,False,Nullを返す)からヒントを得たものです。ところで、この方法はVariantでないと使えません。かといって他の変数型で0や負の値を返したりするとまずい場合も良くあります。特にTrueかFalseかで返したいときにキャンセルが発生したりしたらどうしようか?とよく頭を悩まします(私だけ?)。
解決方法は色々あると思います。下にいくつか考えたものを書いてみます。
'1. True, False, Null
Function foo1(arg As Variant) As Variant
foo1 = True 'True
foo1 = False 'False
foo1 = Null 'キャンセル
End Function
'2. 自分で設定
Const ReturnTrue As Integer = 1
Const ReturnFalse As Integer = 2
Const ReturnCancel As Integer = 3
Function foo2(arg As Variant) As Integer
foo2 = ReturnTrue 'True
foo2 = ReturnFalse 'False
foo2 = ReturnCancel 'キャンセル
End Function
'3. 値は参照渡し、キャンセルかどうかは関数の帰り値
Function foo3(arg As Variant,Check As Boolean) As Boolean
Check = True: foo3 = True 'True
Check = False: foo3 = True 'False
Check = False: foo3 = False 'キャンセル
End Function
'4. MsgBoxの帰り値を流用する
Function foo4(arg As Variant) As vbMsgBoxResult
foo4 = vbYes 'True
foo4 = vbNo 'False
foo4 = vbCancel 'キャンセル
End Function
1.、3.は他の変数形の帰り値にも対応できると思います(1.は内部型として使えるという意味です。宣言はもちろんVariant)。最後の4.はOffice VBAある限りつかえる(?)定数だと思いますので、将来的に混乱せずに使えるのではないでしょうか。どれが良いとは言えませんが、混乱しないために、自分の中である程度統一して使うのが一番重要だと思います。
基本的なところは別のところでごらんいただくとして、今回は使い分けで注意しなければいけないところを
書いてみようと思います。
Sub test()
Dim i As Integer, max As Integer
max = 5
For i = 1 To max
MsgBox i
max = max - 1 '☆
Next i
i = 1: max = 5
Do While i <= max
MsgBox i
i = i + 1
max = max - 1 '☆
Loop
i = 1: max = 5
Do
MsgBox i
i = i + 1
max = max - 1 '☆
Loop While i <= max
End Sub
'☆の行さえなければ、全てのループで同じ挙動をします。この場合、Forループが
カウンタ(i)の設定・チェックを一行でまた自動的にカウンタの増加を増やしてくれるので、
分かりやすいでしょう。Do〜LoopはWhile以下が正しい間、繰り返し実行します。
Whileが書いてあるところでチェックをするので、Do Whileでは入り口ではじかれますが、
Loop Whileでは最後にチェックするので、最低1回は中身が実行されることになります。
ForではDo Whileと同様始めにもチェックします。ですから、このプログラムの場合、
max=0であれば、上の二つは中身が実行されないのに対し、最後のループは1回実行されます。
ここまではたいていの教科書に書いてあると思います。では、上二つの違いはどこでしょう?
ずばり、「Toのあとの式は1度しか評価されない」ということです。つまり、最初にmax=5なので、
「1から5まで実施する」と決まったあとはmaxの値の変化に影響されないのです。
上の例ではForループはi=1〜5間で実行されるのに対し、Do〜Loopではi=1〜3までしか実行されません。
Do〜Loopでは「While以下の式はチェックのたびに評価される」のです。
この違いに気がつかなければ思わぬエラーに会うことになります。
上の例はまだ、原因がつかみやすいのですが、次の例も見てください。
Sub test2()
Dim i As Integer, s As String
s = "teddddst"
For i = 1 To Len(s)
MsgBox Mid(s, i, 1)
If Mid(s, i, 1) = "d" Then
s = Left(s, i - 1) & Right(s, Len(s) - i)
i = i - 1
End If
Next i
MsgBox s
End Sub
文字列内で一致する文字を確認して削除するプログラムです。この結果、s="test"となります。
ところが、毎ループごとに検索文字を表示しているはずなのに何も表示されないパネルが
何回も出てきます。
原因は文字列から文字を抜いて文字列の文字数が減って行くのにLen(s)で始めの文字数分ループを
回してしまうからです。この場合はエラーは出ませんが、場合によっては致命的なエラーになる場合も
出てくるでしょう。
Do〜Loopで回すとうまく行くので、試してみてください。
Microsoftの公式ページを見てると勉強になることがあります。ちょっと見てみると配列を返す関数を作成する方法というのが載っています。C言語では配列や構造体(VBAで言うTypeみたいなものです)で返す方法を用いていましたので、配列についてはすぐに納得しました。
ところがArray()を用いて返す方法というのが載っていますが、何のことかわかりませんでした。Helpを見るとVariantに配列を代入するための関数のようです。JavaScriptのようですね。確認のために以下の関数を作ってみました。
Option Base 1
Function func()
Dim v
v = Array(Array(1, 2), Array(3, 4))
func = v
End Function
Function func2()
Dim a(2, 2)
a(1, 1) = 1: a(1, 2) = 2: a(2, 1) = 3: a(2, 2) = 4
func2 = a
End Function
例えば、A1からB2まで選択したまま=func()とセルに入力して、Ctrl+Shift+Enterと押すと
1 2
3 4
という配列を返します。func2()についても同様です。ArrayはVariantしか扱えない以外は2次元配列と同じように思えます。ところが!
v(1,1)とやるとエラーになるんですね。なんとv(1)(1)という形をとるのです。「vという配列があって、そのうちのv(1)はさらに配列を持っていて、そのうちの1番目の値」という感じです。これはまったく、C言語の配列と同じ扱いです。Variantはさしずめ、Void *型ということですか。(意味が分からない人、ごめんなさい。)
とにかく、配列の扱いが「配列の中の配列」ということで、扱いも違います。
Sub test()
Dim v, w, x
v = Array(Array(1, 2), Array(3, 4))
For Each w In v '一重ではエラーになります。
For Each x In w '1,2,3,4の順に回ります。
MsgBox x
Next x
Next w
End Sub
Sub test2()
Dim a(2, 2), b
a(1, 1) = 1: a(1, 2) = 2: a(2, 1) = 3: a(2, 2) = 4
For Each b In a 'なぜか、1,3,2,4の順に回ります。
MsgBox b
Next b
End Sub
また、配列の中の配列ということで、次のような操作も可能です。
Sub test3()
Dim v, v2
v = Array(Array(1, 2), Array(3, 4))
v2 = v(1) 'Array(1, 2)の部分が代入されます。
MsgBox v2(1) '1を返します。
MsgBox v(1)(1) '同じく1を返します。
End Sub
さらに、2次元配列との違いとして、長方形にならないということです。つまり、
Sub test4()
Dim v
v = Array(Array(1, 2, 3), Array(4, 5), Array(6, 7, 8))
MsgBox v(1)(3) '3
MsgBox v(3)(3) '8
MsgBox v(2)(3) 'Error!
End Sub
こういう事態が起こせるわけです。項目数の変化するリストに使えるかも。
ある時間が来れば、自動的にマクロを実行するという便利な命令があります。OnTimeメソッドです。
expression.OnTime(EarliestTime, Procedure, LatestTime, Schedule)
という仕様になっています(アプリケーションによって若干異なります)。expressionはApplication、EarliestTimeが実行する時間、Procedureが実行するマクロの名前です。詳しくはHelpを見ていただきたいのですが、時間は秒単位でしか指定できず、実行するプロシージャに引数を渡すことができません。
Sub my_Procedure()
処理
Application.OnTime Now + TimeValue("00:00:15"), "my_Procedure"
End Sub
とすれば、15秒ごとに呼び出す関数の出来上がりです。ただし、このままですと永遠に呼び続けるので、何らかの抜け道を作ってやることが必要です。
以前の話でダイアログで進行度を現すプログラムを掲載しました。例えば、2つのタイマーを同時に表示する場合はどうすれば良いでしょう?別に同じフォームをもう一つ作る?いえいえ、そうではないのです。
題にあるようにフォームもユーザー定義のオブジェクトです。ということは、次の命令で実体を複数個作ることが可能です。
Dim t As New TimerForm, t2 As New TimerForm
t.Show: t2.Show
普通の変数とは違うNewとは何でしょう?オブジェクト変数は実体を持ちません。在る場所を指し示すだけです。ですから、まずオブジェクトの実体を作ってやってから、その場所を変数に代入してやる必要があります。その「実体を作って!」というのがNewなのです。次の例を見てください。
Dim t As New TimerForm, t2 As TimerForm
Set t2 = t
tは実体がいるのに対して、t2はtの実体がある場所を指し示すだけですから、t2自体に実体はいりません。だから、Newは必要ないのです。
さて、フォームがユーザー定義のオブジェクトということは、いわば「画像情報のあるクラスモジュール」ということです。ということで、Propertyプロシージャも設定することができます。もっとも必要価値はあまりない(Functionでもかまわない)かもしれませんが、Propertyはプロパティ、Functionはメソッドとして扱われるので、オブジェクト名と「.」を入力した後に自動でプロパティ・メソッドの一覧が出てくるときのアイコンがそれらしく見えるという利点があります(笑)。
というわけで、自動実行を含めた、新しいカウンタープログラムを掲載しますので、見てみてください。
SubプロシージャやFunctionプロシージャ等で受け取る引数にByRef、ByValのキーワードを付けることができます。これは変数の引き渡し方を参照渡しにするか値渡しにするかを決めるキーワードです。(省略したときはByRefになります。)
参照渡し(ByRef)とは変数をそのまま渡します。一方、値渡し(ByVal)は変数のコピーを作って渡します。図書を借り出すかコピーを取って持ち帰るかの違いのようなものです。持ち帰った図書を破ってしまうと今後、読むときに困りますが、コピーをいくら破っても図書自体には何の影響もありません。同様に、ByRefで渡した変数を書き換えると呼び出した元の関数の変数も変わってしまいますが、ByValで渡した場合は元の関数の変数に変化は及びません。
ByValの方が関数の独立性が保つためにはよりよい方法ですが、元の関数に変化した変数を返したいときには不便ですし、コピーのときに若干時間がかかります。また、BASICという言語はもともと参照渡しが基本でしたので、VBAにおいても、省略したときにはByRefが基本となっています。
一方、ByValは引数の型チェックが甘く、コンパイル時にはチェックされません。Variantの変数をIntegerの引数に渡すことも可能です。変換できない場合は実行時エラーとなります。(ByRefはコンパイルエラーとなります。)
ところで、Range等のオブジェクト変数はどうでしょう?ByValで渡すとどこかにセルを作って渡してくれるのでしょうか?
実はセル(オブジェクト)のコピーは作成されません。ByRefで渡してもByValで渡してもr.Value=2とすれば元の関数の変数で指し示していたセルの内容が変わってしまいます。それではオブジェクト変数ではByRef、ByValのキーワードが無効なのでしょうか?
オブジェクト変数とは以前書きましたが、それ自身は実体を持ちません。在る場所を指し示しているだけです。つまり、名刺のようなものです。そして引数のByRefとByValの違いは名刺自体を渡すかコピーを取って渡すかの違いといえます。
名刺を参考に会いに行けば、名刺自体だろうがコピーだろうが本人に会いに行くことができます。ところが、その名刺の例えば部署名を書き換えてしまったら、本人に会いに行くことができなくなります。元の関数の変数にまでその変化が及ぶのがByRefでそうではないのがByValなのです。次のプログラムを参考にしてみてください。
Sub test() Dim r As Range Set r = ActiveSheet.Range("A1") subtest01 r MsgBox r.Address & ": " & r.Value, , "ByRef" '$A$2: 2 Set r = ActiveSheet.Range("A1") subtest02 r MsgBox r.Address & ": " & r.Value, , "ByVal" '$A$1: 3 End Sub Sub subtest01(ByRef r As Range) r.Value = 1 Set r = r.Offset(1) r.Value = 2 End Sub Sub subtest02(ByVal r As Range) r.Value = 3 Set r = r.Offset(1) r.Value = 4 End Sub
ある人物の年齢を調べる。さらに、子供たち一人一人の年齢を調べる。さらにその子供の子供の年齢を…
こういう場合、再帰呼び出しという手法を使います。詳しく、このルーチンを考えて見ると、
Sub NewItems() Dim App As Outlook.Application Dim NowFolder As Outlook.MAPIFolder Dim Result As String Set App = CreateObject("Outlook.Application") Set NowFolder = App.ActiveExplorer.CurrentFolder '現在のフォルダの取得 Result = "" NewItemFinder NowFolder, Result 'サブプロシージャの呼び出し If Result <> "" Then '結果の表示 MsgBox "件数" & vbTab & "フォルダ" & vbCr & Result Else MsgBox "新着アイテムはありません。" End If End Sub Private Sub NewItemFinder(ParentFolder As Outlook.MAPIFolder, Result As String) Dim ChildFolder As Outlook.MAPIFolder If ParentFolder.UnReadItemCount > 0 Then '未読アイテム数の確認 Result = Result & vbCr & ParentFolder.UnReadItemCount _ & vbTab & ParentFolder.Name End If If ParentFolder.Folders.Count > 0 Then '子フォルダの確認 For Each ChildFolder In ParentFolder.Folders NewItemFinder ChildFolder, Result '子フォルダに対してサブプロシージャ Next ChildFolder '(このプロシージャ自身)の呼び出し End If End Sub