読者です 読者をやめる 読者になる 読者になる

t-hom’s diary

主にVBAネタを扱っているブログです。

VBA クラスモジュールでRangeの仕組みを説明する

昨日公開した以下の記事について、比喩ではちょっと難しいという声をいただいたので、今回はクラスモジュールを使って私が想像するRangeの仕組みを具体的なコードで解説しようと思う。
thom.hateblo.jp

まずはSheetの模擬クラスとRangeの模擬クラスを用意する。
本物と同じ名前にしてしまうと、メインコードで使う際にちゃんと自作の方を使ってるの?という疑惑につながるので、オブジェクトの名前はオリジナルにしておこう。

Sheetの代わりにSheeeet、Rangeの代わりにoRange(オレンジ)。
f:id:t-hom:20170417214655p:plain

まずSheeeetのコードはこちら

Private table(1 To 9, 1 To 9) As String

Property Get oRange(adddresss As String) As oRange
    Dim ret As oRange: Set ret = New oRange
    Dim x As Long: x = Ato1(Left(adddresss, 1))
    Dim y As Long: y = CLng(Right(adddresss, 1))
    ret.Init Me, x, y
    Set oRange = ret
End Property

'列のアルファベット(A~I)を列番号1~9に変換する関数
Function Ato1(char As String) As Integer
    Ato1 = InStr(1, "ABCDEFGHI", char)
End Function

Friend Property Let Value(r As Long, c As Long, v As String)
    table(r, c) = v
End Property

Friend Property Get Value(r As Long, c As Long) As String
    Value = table(r, c)
End Property

次にoRangeのコード

Private parent_ As Sheeeet
Private row_ As Long
Private column_ As Long

Sub Init(p As Sheeeet, r As Long, c As Long)
    If parent_ Is Nothing Then
        Set parent_ = p
        row_ = r
        column_ = c
    Else
        Err.Raise vbObject, "oRange", "このoRangeはすでに初期化されています。"
    End If
End Sub

Property Let Value(v As String)
    parent_.Value(row_, column_) = v
End Property

Property Get Value() As String
    Value = parent_.Value(row_, column_)
End Property

そして標準モジュールに書くメインコードがこちら

Sub Rangeモドキ実験()
    Dim sh As Sheeeet: Set sh = New Sheeeet
    
    'まずは普通に値を設定して表示
    sh.oRange("A1").Value = "Hello!"
    sh.oRange("B3").Value = "GoodBye!"
    MsgBox sh.oRange("A1").Value
    MsgBox sh.oRange("B3").Value
    
    'オブジェクト比較でFalseになる実験
    MsgBox sh.oRange("A2") Is sh.oRange("A2")
    
    'oRange型変数に入れて使用
    Dim r1 As oRange
    Set r1 = sh.oRange("C4")
    Dim r2 As oRange
    Set r2 = sh.oRange("C5")
    r1.Value = "Konnichiwa"
    r2.Value = "Sayonara"
    
    MsgBox r1.Value
    MsgBox r2.Value
End Sub

これでメインコードを実行すると、本物のRangeオブジェクトと同じようにValueの設定、取得ができる。
まあ偽物なので定義した以上の機能はないけど。本物のシートに書き込まれるわけではなく、シートを模したSheeeetオブジェクト内の配列に値が設定されるだけ。

さて、解説だけど全部は面倒なのでポイントだけ。
まずSheeeetクラスの以下の1行。

Private table(1 To 9, 1 To 9) As String

これが9×9のセルのを模したもの。実際のシート上の値はここに保存される。
しょぼいけどまぁ説明用サンプルなので。

次にSheeeetクラスのoRangeプロパティ。

Property Get oRange(adddresss As String) As oRange
    Dim ret As oRange: Set ret = New oRange
    Dim x As Long: x = Ato1(Left(adddresss, 1))
    Dim y As Long: y = CLng(Right(adddresss, 1))
    ret.Init Me, x, y
    Set oRange = ret
End Property

プロパティ名のoRangeと型名のoRangeはたまたま同じ名前になってるだけなので注意。これがRangeの理解がややこしくなる原因の一つ。名前が一緒なのでプロパティとオブジェクトを混同しがち。

このプロパティプロシージャでは内部でoRangeオブジェクトを生成して、それを返している。
ret変数に新規のoRangeオブジェクトを格納し、そのoRangeオブジェクトのInitプロシージャにMe(つまりSheeeetオブジェクト自身)を渡している。

そしてoRangeオブジェクトのInitプロシージャではこれを仮引数pで受け取り、自身のparent_変数にセットしている。このときに行と列の情報も受け取って自分のrow_とcolumn_に代入する。

Sub Init(p As Sheeeet, r As Long, c As Long)
    If parent_ Is Nothing Then
        Set parent_ = p
        row_ = r
        column_ = c
    Else
        Err.Raise vbObject, "oRange", "このoRangeはすでに初期化されています。"
    End If
End Sub

これでoRangeが準備できたので、Valueプロパティで値をいじるとparent_変数にセットしたSheeeetオブジェクトのValueプロパティが操作される。

Property Let Value(v As String)
    parent_.Value(row_, column_) = v
End Property

Property Get Value() As String
    Value = parent_.Value(row_, column_)
End Property

つまりデータはあくまでSheeeetオブジェクト内にあり、oRangeは自身が参照を保持しているSheeeetオブジェクトのプロパティを通じて操作しているということ。

ちなみにSheeeet側のValueプロパティはFriendスコープにしているけれど、今回SheeeetのValueはoRangeによってのみ操作されるのであって、メインコードから直接操作はしませんよという意味づけのためにFriendスコープにした。
Friendは同じプロジェクト内でのみ参照できるというスコープである。今回のメインコードでは同じプロジェクト内に書いたので特に操作を制限するといった効果はない。

Excelブック1つごとに1つのプロジェクトしか作れないので、Friendスコープを活かそうと思ったらアドイン化して参照設定するしかないけれど。。
【参考】
thom.hateblo.jp

4/18追記

複数セルは?と突っ込まれそうなので予め言い訳しておくと、複数セルに対応させるとコードの複雑度が増すので今回はあえて単一セルのみサポートするRange型もどきとした。
Range型はエージェントのように機能するという点を解説するにはこれで十分なので。

ちなみに複数セルに対応するには、oRange型の内部で座標をコレクションか配列で持たせ、Valueにアクセスされたら座標をForかFor Eachでまわしながら連続でSheeeetにアクセスすれば良い。広い範囲を扱う場合は開始セルと終了セルを持たせておけばOK。ただし飛び地を扱うにはもう一工夫必要。などなど考えていくと結構面倒なコードになる。

当ブログは、amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、 Amazonアソシエイト・プログラムの参加者です。