昨日公開した以下の記事について、比喩ではちょっと難しいという声をいただいたので、今回はクラスモジュールを使って私が想像するRangeの仕組みを具体的なコードで解説しようと思う。
thom.hateblo.jp
まずはSheetの模擬クラスとRangeの模擬クラスを用意する。
本物と同じ名前にしてしまうと、メインコードで使う際にちゃんと自作の方を使ってるの?という疑惑につながるので、オブジェクトの名前はオリジナルにしておこう。
Sheetの代わりにSheeeet、Rangeの代わりにoRange(オレンジ)。
まず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
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
MsgBox sh.oRange("A2") Is sh.oRange("A2")
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。ただし飛び地を扱うにはもう一工夫必要。などなど考えていくと結構面倒なコードになる。