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

Class Moduleについて

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

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


目次

  1. クラスモジュールを作ってみよう
  2. クラスモジュールを作ってみよう(2)
  3. クラスモジュールを使ってみよう。その2
  4. クラスモジュールを使ってみよう。その2(2)
  5. クラスモジュールを使ってみよう。その2(3)
  6. 参照渡し

Office VBAの部屋に戻る

クラスモジュールを作ってみよう(2001/9/7)

クラスとは変数管理などを簡単にするために作っておく窓口のようなものです。あえて作る必要もないけど、手順の統一・簡略化に威力を発揮するといったところでしょうか。私の中では。例えば、一時的に記憶するためのセルを作るために

  1. 一時的にワークシートを作成する。
  2. 範囲を取得する。
  3. 使い終わったら、ワークシートを削除する。

という手順を踏むと思いますが、いちいち書くのが面倒だったり、シートを削除する手順を忘れたりすると思います。クラスはクラス内にある変数へのアクセス方法を管理するだけでなく、クラスを作成した時点と不要になった時(クラスを宣言したときのプロシージャ等の範囲(スコープ)からプログラムが抜け出した時)に必要な処理を自動で行わすことができます。

プログラム(クラスモジュール内)

Public rng As Range

Private Sub Class_Initialize()
    Dim temp As Range
    Set temp = Selection    'ワークシートを作成した時点で選択範囲が変わるので先に記憶しています。
    Set rng = ThisWorkbook.Worksheets.Add.Cells(1)
    temp.Worksheet.Activate '先に記憶した選択範囲を選択しなおします。
    temp.Select
End Sub

Private Sub Class_Terminate()
    On Error Resume Next        'Worksheetが削除されている場合のためです。
    Application.DisplayAlerts = False
    rng.Worksheet.Visible = xlSheetVisible  '可視状態でないと削除できないようです。
    rng.Worksheet.Delete
    Application.DisplayAlerts = True
End Sub

このクラスモジュールは一時保管用の範囲を作成します。呼び出しと同時にワークシートを作成してA1セルの範囲を変数rngに設定します。終わるときはワークシートを削除します。例えば、このクラスモジュールの名前をRangeClassという名前にすると、

プログラム(標準モジュール内)

Sub test()
    Dim temp As New RangeClass
    Selection.Cells(1).Copy temp.rng
    Selection.Cells(2).Copy Selection.Cells(1)
    temp.rng.Copy Selection.Cells(2)
End Sub

これだけで、選択範囲の1番目と2番目のセルを書式ごと交換するプログラムの出来上がりです。temp.rngを呼び出した時点で自動的にワークシートが作成され、temp.rngで一時保管用の範囲を表します。プロシージャが終わった時点で自動的に一時保管用のワークシートは削除されます。temp.rngはただのワークシート上のセルへの参照ですから、セルに対する操作が全てできます。これが実は問題で、Set temp.rng = Nothingとでもされてしまった日には正常に終了できなくなります。このような脆弱性をなくすためにクラスにはProperty Set/Let/Getというプロシージャが存在します。続きはまた次回。

おまけ
Withの性質によって一時的に範囲を保存しておくこともできます。次のプログラムは上の物と同じ動作をします。

プログラム(クラスモジュール内)

Private Sub Class_Initialize()
    With Selection
        Set rng = ThisWorkbook.Worksheets.Add.Cells(1)
        .Worksheet.Activate
        .Select
    End With
End Sub

ただ、見た目に何をしているのか良くわからないのでお勧めはしません。


目次に戻る

クラスモジュールを作ってみよう(2) (2001/9/14)

前回、一時保管用のセル範囲を簡単に管理するクラスを作りましたが、クラス内のRange変数が自由に扱えるために保存用の範囲とちがう範囲を指定しなおせるという欠点がありました。クラスの内部変数が自由に扱えるということは例えて言えば、銀行で預金客が引き下ろしにきたら、金庫を開けて出し入れさせるようなものです。クラスモジュール内ではプロパティという内部変数を取り扱う窓口を作ることができます。このプロパティはVBAにはじめからあるオブジェクトのプロパティと同じように使うことが出来ます。前回のプログラム中の

Public rng As Range

の部分を次のように替えてください。
Private rng As Range

Public Property Get r() As Range
    Set r = rng
End Property

rngを外から触れないようにして、代わりにrという窓口を作りました。このプロパティは一時保管用のワークシートの左上のセルを返します。例えば、
    Dim temp As New RangeClass
    temp.r = 2

とすることによって、一時保管用のセルを作って2を代入します。
このプロパティは範囲の参照は出来ますがSetは使えませんので、間違えて参照先を変更することがなくなります(エラーになります)。よくVBAのヘルプにある「値の取得のみ可能です。」を実現していることになります。
(これでクラス内のエラーが全て防げるかというとそうではありません。temp.r.Worksheet.Deleteという禁断の技(笑)が存在します。この技を防ぐにはセル範囲を返すのではなく、セル範囲の値等を返すという手段をとるしかなくなると思います。それでは面倒くさいし、機能が制限されてしまうので、そこまで安全策は取っていません。Worksheetを参照出来なくするような、何か良い方法があれば教えてください。)

さて、ここでもうひとつサンプルプログラムを作ってみました。コピーした範囲と大きさのちがう範囲にペーストしたい場合に使えます。マクロボタンを作って使ってください。セル範囲をコピーし、ペーストしたい範囲を選択してマクロボタンを押します。

プログラム(標準モジュール内)

Sub ResizePaste()
    On Error Resume Next
    Dim temp As New RangeClass
    Dim nowSelection As Range
    Set nowSelection = Selection
    With temp.r
        .PasteSpecial
        .Resize(nowSelection.Rows.Count, nowSelection.Columns.Count).Copy
    End With
    With nowSelection
        .Activate
        .PasteSpecial
    End With
End Sub


目次に戻る

クラスモジュールを使ってみよう。その2(2001/12/13、12/14修正)

前回(9/14)にも書きましたが、クラスモジュールは操作のための窓口を作ってやろうというものです。私が良く見に行っているSOFT-XさんのHPでShapeのTextは256文字以上を一度に書き込み/読み込みすることができない、ということが書かれていましたので、これをクラスモジュールで簡単に読み書きできるようにしてみましょう。

プログラム(クラスモジュール内(Class1をShapeTextとしてください))

Option Explicit

Private MyShape As Shape
Private Const Limit As Long = 254

Property Set Shape(Target As Shape)
    Set MyShape = Target
End Property

Property Get Shape() As Shape
    Set Shape = MyShape
End Property

Property Let Text(Text As String)
    Const Dummy As String = "a"
    Dim l As Long, Repeat As Long, Pos As Long
    If Not MyShape Is Nothing Then
        With MyShape.TextFrame
            Repeat = Len(Text) \ Limit
            .Characters.Text = ""
            For l = 0 To Repeat
                Pos = l * Limit + 1
                .Characters(Pos).Insert Mid(Text, Pos, Limit)
            Next l
        End With
    End If
End Property

Property Get Text() As String
    Dim l As Long, Repeat As Long
    Dim temp As String
    If Not MyShape Is Nothing Then
        With MyShape.TextFrame
            Repeat = .Characters.Count \ Limit
            For l = 0 To Repeat - 1
                temp = temp & .Characters(l * Limit + 1, Limit).Text
            Next l
            If .Characters.Count Mod Limit <> 0 Then
                temp = temp & .Characters(l * Limit + 1).Text
            End If
        End With
    End If
    Text = temp
End Property

Property Let Characters(Optional Pos As Long = 0, Optional Length As Long = 0, Text As String)
    Dim temp As String
    If Not MyShape Is Nothing Then
        temp = Me.Text
        If Pos = 0 Then
            Me.Text = temp & Text
        Else
            Me.Text = Left$(temp, Pos - 1) & Text & Right$(temp, Len(temp) - Pos - Length + 1)
        End If
    End If
End Property

Property Get Characters(Optional Pos As Long = 0, Optional Length As Long = 0) As String
    Dim temp As String
    If Not MyShape Is Nothing Then
        temp = Me.Text
        If Pos = 0 Then
            Characters = temp
        Else
            Characters = Mid$(temp, Pos, Length)
        End If
    End If
End Property

先におまけのほうから見てみましょう。Charactersは文字列から一部分を取り出すプロパティです。引数を何も設定しない(Pos=0)と全ての文字列を取り出します(後述のTextと一緒)。通常は位置(Pos)と長さ(Length)を指定して使用します。逆に設定することも可能です。この場合、Pos=0は文字列の最後を示します。つまり、指定した文字列(Text)を現在の文字列に追加します。通常は位置(Pos)と長さ(Length)を指定して、その部分を指定した文字列(Text)で置き換えます。Length=0の場合、置き換えずに指定位置の直前に指定文字列を挿入します。
しかし、見ていただいたら分かりますが、このプロパティは通常の文字列操作をしているだけに過ぎません。Me.TextでShapeの文字列を普通に扱えることがミソなのです。
詳しいプログラムの説明は省きますが、Property Let/Property Getの組で読み書きできるプロパティを作ることが出来ます。これを使って、Shapeプロパティで目標を定めた後、Textプロパティで普通に読み書きすることが出来ます。以下にサンプルプログラムを示します。

プログラム(標準モジュール内)

Sub test()
    Dim s As Shape, temp As String
    Dim st1 As New ShapeText, st2 As New ShapeText
    Const Length As Integer = 254
    With ActiveSheet
        Set s = .Shapes.AddShape(msoShapeRectangle, 20, 20, 150, 300)
        s.TextFrame.Characters.Text = String(Length, "a")
        Set st1.Shape = .Shapes.AddShape(msoShapeRectangle, 200, 20, 150, 300)
        st1.Text = String(Length, "a")
        Set st2.Shape = .Shapes.AddShape(msoShapeRectangle, 370, 20, 150, 300)
        st2.Text = st1.Text
        st2.Characters(3, 4) = st1.Text
        st2.Text = st2.Text + st1.Text
        st2.Shape.TextFrame.Characters.Font.Bold = True
        MsgBox Len(st2.Text)
    End With
End Sub

どうですか?何も考えずに、普通のTextとして扱えるようになっていますね。クラスモジュールさえ他のワークブックに持っていけば、いくらでも再利用することが出来ます。これが、クラスモジュールの便利なところなのです。


目次に戻る

クラスモジュールを使ってみよう。その2(2) (2001/12/17)

Property Get/Letについて詳しく説明をして欲しいというご意見をいただきましたので、2回ぐらいに分けて説明したいと思います。

PropertyはVBAのオブジェクトでおなじみの「プロパティ」を自分のクラス(フォームでも作ることが可能なようです。)に作るものです。そのクラスに必要なパラメータの値を設定/取得するための動作を記述するものです。利点は次のようなものが挙げられます。

  1. パラメータに対する不正な値の入力をブロックする。(入力自体をブロックすることも可能)
  2. 値の変化に伴う処理を同時に行うことが出来る。
  3. 入出力に同じ名前のプロシージャを用いることが出来、普通の変数のように扱うことが出来る。(式の左辺・右辺に用いることが出来る。)

つまり、プログラムの安定性・扱いやすさを考えたとき、Propertyは重要になると思われます。一方、欠点は、
  1. わざわざ使うまでも無い(Sub/Functionで十分。Property Get/Letを覚えるのが面倒。)
  2. 入出力で別々に記述する必要がある

ということがあります。(特に欠点1は大きいでしょう。クラスを使うこと自体面倒という話もありますが。)

ここで、利点1,2の例を示しましょう。

プログラム(クラスモジュール内(Class1))

Option Explicit

Private Age As Integer
Private Up20 As Boolean

Property Let 年齢(i As Integer)
    If i >= 0 Then Age = i Else Age = -1
    Up20 = (Age >= 20)
End Property

Property Get 年齢() As Integer
    年齢 = Age
End Property

Property Get Is成人() As Boolean
    Is成人 = Up20
End Property

Property Get IsError() As Boolean
    If Age = -1 Then IsError = True Else IsError = False
End Property

このクラスで重要なAgeはPrivate指定なので、直接は触れませんね。そこで、入出力用のPropertyを用意してあげるのですが、0以下というのはありえないので、全てエラー値(-1)にしてしまいます(利点1)。成人かどうかは年齢によって変化しますので、Ageを設定したときに一緒に変化するようにプログラムすれば使いやすいですね(利点2)。また、成人かどうかは年齢によって決まるものですし、エラーかどうかもその値によって決まるものであって、設定するものではありませんね。だから、Property Letを用意していません。これにより、「取得のみ可能」なプロパティとなります。IsErrorについても同様です(利点1)。利用例は以下のとおりになります。

プログラム(標準モジュール内)

Sub test1()
    Dim a As New Class1
    a.年齢 = InputBox("入力してください。")
    If a.IsError Then
        MsgBox "入力エラー"
    Else
        MsgBox "2倍の年だと" & a.年齢 * 2 & "歳"
    End If
    MsgBox a.Is成人, , "成人?"
End Sub

どうですか?「a.年齢」というのがひとかたまりで、普通の変数のように使われていますね。式の右辺だけではなく、左辺にも持ってくることが出来ます。逆に設定が出来ない(左辺に持ってくることが出来ない)ようにすることも出来ます。入出力をひとつの命令で行うことが出来、エラー値の処理が出来て、不正な値の代入を防げる、というのがPropertyの利点だということが分かっていただけると思います。
ところで欠点ですが、わざわざプロパティにする必要があるのか?というところに尽きると思います。実際問題として、Functionを用いれば、大体のところは代用することが出来ます。次をご覧下さい。

プログラム(クラスモジュール内(Class2))

Option Explicit

Private Age As Integer

Public Function Func年齢(Optional i As Integer = -1) As Integer
    If i >= 0 Then Age = i
    Func年齢 = Age
End Function

Property Get 年齢() As Integer
    年齢 = Age
End Property

Property Let 年齢(i As Integer)
    Age = i
End Property


プログラム(標準モジュール内)

Sub test2()
    Dim a As New Class2, b As New Class2
    
    a.Func年齢 20
    b.Func年齢 a.Func年齢 + 10
    MsgBox b.Func年齢
    
    a.年齢 = 20
    b.年齢 = a.年齢 + 10
    MsgBox b.年齢

End Sub

「=」があるかないかだけの違いといえばそれまでなんですね。ですから、Propertyの作り方のコツは、「いかに見ただけで使い方が分かるか。」という点になると思います。値はGet/Putのように関数が分かれていたほうが分かりやすいという人も多いかもしれませんが、たとえば、下の例の「Text」だったら、Property名を見ただけで、使い方が浮かんで、実際そのとおりに使えますよね。これが、Propertyの効用なんじゃないでしょうか。


目次に戻る

クラスモジュールを使ってみよう。その2(3) (2001/12/19)

前々回(12/13)のプログラムでCharactersのプロパティを作成したのですが、次のような質問がきました。
「標準モジュールで関数を作るとき、引数に Optional を指定すると、それ以降の引数はすべて Optional 指定をしないとコンパイラが受けつけてくれません。なぜCharactersではそうではないのですか?」

それでは早速、Property LetとGetの最初の一行(宣言部分)、実際の使用例を並べて書いてみましょう。

Property Let Characters(Optional Pos As Long = 0, Optional Length As Long = 0, Text As String)
Property Get Characters(Optional Pos As Long = 0, Optional Length As Long = 0) As String

s.Characters(3,4) = string	'Property Letが実行される。
string = s.Characters(3,4)	'Property Getが実行される。
(ちなみに、Letの一番最後を除いてLet/Getでまったく同じ引数を宣言していますね。また、Letの一番最後の引数とGetの帰り値の型が一致しています。これは式の右辺でも左辺でも同じように使えるための約束事です。)

たしかにProperty Letにおいて、1,2番目の引数はOptional指定なのにLetの一番最後の引数はそうではありませんね。なぜなんでしょう?

実際の使用例と宣言の引数を比較してみて、Posが3でLengthが4、そしてstringがText(およびGetの帰り値の型)に一致していることが分かるでしょうか?つまり、最後の引数だけ役割が違うのです。見た目で話すと

Property Letの最後の引数は代入される右辺の値で、その他は「関数」の引数である

ということになります。もちろん、Function(関数)ではなくProperty Letなのですが、最後の引数以外についてはご質問のとおり、関数と同じ制約(一度Optionalを指定した以降は全てOptionalにしなければならない)を受けます。しかしながら、最後の引数は代入値という、ほかの引数とは異なった性格を持つ、Property Letには絶対必要なものです。ですから、逆にProperty Letの最後の引数はOptional指定をすることが出来ない引数なのです。

お分かりいただけたでしょうか。


目次に戻る

参照渡し(2002/2/2)

VBAでは私の知る限り、数値・文字列は参照渡しができません。と書くと、「関数間では参照渡し(ByRef)が既定だよ。」といわれるかもしれませんが、関数内では参照渡しができないので、例えば、呼び出されたサブルーチンのほうで、参照を維持することができません。
j = iは値渡しです。一方、Set j = iは参照渡しなのですが、数値、文字列に適用しようとすると「オブジェクトが必要です。」とエラーが出ます。ここで思いつきました。「クラスを使ってやろう。」
次を見てください。

Option Explicit

Sub test()
    Dim i As Integer, j As Integer
    i = 1
    j = i   'Set j = iはエラー
    i = 2
    MsgBox j    '1
End Sub

Sub test2()
    Dim i As New IntegerClass, j As IntegerClass
    i.Value = 1
    Set j = i
    i.Value = 2
    MsgBox j.Value  '2
End Sub

Sub subtest3(Optional i As IntegerClass = Nothing)
    Static j As IntegerClass
    If Not i Is Nothing Then
        Set j = i
    ElseIf Not j Is Nothing Then
        MsgBox j.Value  '2
    End If
End Sub

Sub test3()
    Dim i As New IntegerClass
    i.Value = 1
    subtest3 i
    i.Value = 2
    subtest3
End Sub
IntegerClassというクラスを作り、そこには一行、「Public Value As Integer」と書いてやれば終わりです。もちろん、StringでもVariantでもOKです。
最後の例を見ると親ルーチンの値が変化したことが、サブルーチンで読み取れることがわかると思います。
これを使うメリットは渡す変数を覚えていなくてもいい点、Public変数にしなくても共有化できる点などがあげられます。が、これらはそのまま、構造化プログラミングという観点ではデメリットであり、多用は避けましょう。


目次に戻る

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