昨日公開した以下の記事について、比喩ではちょっと難しいという声をいただいたので、今回はクラスモジュールを使って私が想像する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 '列のアルファベット(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。ただし飛び地を扱うにはもう一工夫必要。などなど考えていくと結構面倒なコードになる。