いらっしゃいませ、277612番目のお客様。  

実用(?)ヒント集

このページに含まれるプログラム、ファイル等については、

  1. 一応、著作権は放棄してません。転載等される方は私にご連絡ください。
  2. 内容については万全を期していますが、利用については自己責任でお願いします。いかなる損害にも責任を負いません。
  3. 動作確認は、(1)Windows 98+Excel 2000; (2)Windows XP+Excel 2002で行っています。
以上の点にご注意願います。


目次

  1. Withの罠
  2. 変数は使う前に定義しよう
  3. 文字列結合は"&"で。
  4. 変数の帰り値
  5. For〜NextとDo〜Loop
  6. v(1)(1)って分かります?
  7. 関数の自動実行
  8. フォームもオブジェクト
  9. ByRef,ByVal
  10. 再帰呼び出し

Office VBAの部屋に戻る

Withの罠(2001/9/7)

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は単にプログラムを見やすくするためのお約束ではなく、対象オブジェクトの指定というれっきとしたプログラムの一部であることを忘れず、むやみやたらと使わないようにしましょう。


目次に戻る

変数は使う前に定義しよう(2001/10/20)

よく言われることです。短いプログラムなら、めんどくさいでしょうが。Office VBAではOption Explicitとプログラムの頭に記述することにより、変数の宣言(定義)が義務化されます。
変数の名前の付け方にもいろいろ流儀があると思いますが、私はOffice VBAでは次のようにしてプログラミングしています。

こうすることによって、入力ミスをしたときに小文字から大文字に変わらないので、入力ミスしたことが分かるのです。これで「変数testに代入したつもりだったのに、変数textを作って代入していた。」ということを視覚的に防ぐことが出来ます。前述のOption Explicitの宣言をあわせれば、完全ですね。
変数の宣言ですが、どうせ宣言するなら、Variant以外で宣言しましょう。どうしてもVariantでないといけないという場合もありますが、そうでない場合、Variant以外で宣言すれば、次の恩恵が受けられます。 さて、入力が簡単になるとはどういうことでしょう。例えば、BorderオブジェクトのLineStyleプロパティに次のような説明がかかれています。

罫線または輪郭線の種類を設定します。使用できる定数は、XlLineStyle クラスの xlContinuous、xlDash、xlDashDot、xlDashDotDot、xlDot、xlDouble、xlSlantDashDot、xlLineStyleNone のいずれかです。値の取得および設定が可能です。バリアント型 (Variant) の値を使用します。

このとき、LineStyleプロパティをmyBorder.LineStyle = lsのように変数lsで設定しようとしています。このとき、lsはどのように宣言したらよいでしょうか。Variantで宣言するしかないでしょうか?

答えは、
Dim ls As XlLineStyle

です。こう宣言しておくことによって、ls = と入力しただけで、 使用できる定数の一覧が出てくるので、あとは選択するだけで入力終わり。 非常に簡単で、かつ、間違えません。


目次に戻る

文字列結合は"&"で。(2001/10/27)

文字列(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

これは、「足し算じゃなくて、文字列の結合なんだよ」という記号です。見た目にも足し算と違うことが分かるので良いのではないでしょうか。


目次に戻る

変数の帰り値(2001/11/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ある限りつかえる(?)定数だと思いますので、将来的に混乱せずに使えるのではないでしょうか。どれが良いとは言えませんが、混乱しないために、自分の中である程度統一して使うのが一番重要だと思います。


目次に戻る

For〜NextとDo〜Loop(2001/11/15)

基本的なところは別のところでごらんいただくとして、今回は使い分けで注意しなければいけないところを 書いてみようと思います。

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で回すとうまく行くので、試してみてください。


目次に戻る

v(1)(1)って分かります?(2002/1/26)

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
こういう事態が起こせるわけです。項目数の変化するリストに使えるかも。


目次に戻る

関数の自動実行(2002/2/16)

ある時間が来れば、自動的にマクロを実行するという便利な命令があります。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秒ごとに呼び出す関数の出来上がりです。ただし、このままですと永遠に呼び続けるので、何らかの抜け道を作ってやることが必要です。


目次に戻る

フォームもオブジェクト(2002/2/16)

以前の話でダイアログで進行度を現すプログラムを掲載しました。例えば、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はメソッドとして扱われるので、オブジェクト名と「.」を入力した後に自動でプロパティ・メソッドの一覧が出てくるときのアイコンがそれらしく見えるという利点があります(笑)。
というわけで、自動実行を含めた、新しいカウンタープログラムを掲載しますので、見てみてください。


目次に戻る

ByRef,ByVal(2002/3/30)

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

同じようにセルが変化しますが、ByRefで呼び出すとtest関数内のrの指し示すセルが変化しているのがわかると思います。


目次に戻る

再帰呼び出し(2002/4/27)

ある人物の年齢を調べる。さらに、子供たち一人一人の年齢を調べる。さらにその子供の子供の年齢を…
こういう場合、再帰呼び出しという手法を使います。詳しく、このルーチンを考えて見ると、

  1. 初期化して、人物に関してプロシージャを呼び出す。
  2. 年齢を調べる。
  3. 子供がいるかどうかを確認する。
  4. 子供がいたら、子供全員に関してプロシージャを呼び出す。(2に戻る)
  5. 子供がいなかったら、プロシージャの処理を終わる。(親の4に戻り、次の子の処理に移る)
  6. 全部の人物のチェックが終了したら、終了の処理をする。
1,6を呼び出されたプロシージャで処理して、2-5を再帰呼び出しのプロシージャにすると、すっきりしたプログラムになります。ここでは、Outlookのプログラムとして、フォルダ一覧で選んだフォルダの中にある新着アイテムの数をチェックするプロシージャ(NewItems)を載せます。「こんなものか」程度に見てください。

プログラム

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


目次に戻る

にゃま夫のへや   ・Office VBA   ・おもちゃ箱   ・iαppli
ホームページに戻る。
無断転載を禁じます。
にゃま夫