列符号取得が可能なRangeオブジェクトのラッパークラス
きっかけ
「列番号はColumプロパティで簡単に取得できるのに、列符号って取り出しにくいよなー」と思って、コチラを参考にXDF列までに対応した自作関数を作ったのがそもそもの始まり。
他のよく使う機能を一つのモジュールにまとめて使い回していたんだが、モジュール内のプロシージャが増えるにつれ、管理がいい加減になってしまって、放置プレーになってしまっていた。
ブックAに仕込んだ○○プロシージャは修正したけど、ブックBの○○プロシージャは以前のまま、みたいな。
ちょうどクラスモジュールの練習中でもあるので、
一つのセルを包み込んで、列符号を尋ねたら応えてくれて、なおかつRangeオブジェクトの諸機能もそのまま使える
クラスを作ってみようと思い立った。
これを「ラッパークラス」と呼んでよいものかどうかは自信がないので、達人のみなさん、ツッコミよろしく!
準備
クラスモジュールを挿入して、オブジェクト名を「WrappedCell」にする。
リスト1-1 宣言セクション
Option Explicit '自作エラー情報のための構造体 '……(1)' Private Type errorType Number As Long Description As String End Type '拡張できるように配列で宣言 '……(2)' Private myError() As errorType 'エラーインデックス用の番号を列挙体で準備 '……(3)' Private Enum errIndex CELL_NOT_SINGLE = 0 'セルが複数渡された CELL_NOT_GOT 'セル未取得 End Enum
リスト1-1の説明
まずは下ごしらえ。普通はフィールドの宣言から始めるけど、今回はちょっと違う。
- (1)では、自作エラーを表示するために、ErrオブジェクトのRaiseメソッドの引数用の構造体を準備した。まあ、たった2つのパラメータごときを構造体にまとめるというのは鶏を割くに牛刀を用いるの観なきにしもあらずだが、使わないと忘れるので。
- (2)では、(1)で準備したerrorType型の変数myErrorを配列で準備している。あとで自作エラーの種類を増やすときに楽なようにこうした。他にもエラーを吐かせるべきことがあるかも知れんので。いわゆるYAGNIの原則には反しますがw
- (3)では、 id:imihitoさんのこのとき のアドヴァイス
処理のブロックに番号をつけるときに列挙型(Enum)を使うと、識別子を付けられるのでただの数字より見やすくなってオススメ
を生かして配列myErrorのインデックス番号を列挙体で準備した。今後、エラーの種類を増やすときは、ここで番号に意味のある文字列を割り振ったら良い。
リスト1-2 フィールド
'フィールド Private oneSelf_ As Range Private columnLetter_ As String
リスト1-2の説明
とりあえず、包み込む対象のセルそのものと、そのセルの列符号をフィールドとして持たせることにした。他になんかあるだろうか???
リスト1-3 アクセサ(1)
'アクセサ Public Property Set oneSelf(ByRef newCell As Range) If newCell.Count <> 1 Then '……(1)' Call raiseError(errIndex.CELL_NOT_SINGLE) '……(2)' Exit Property End If Set oneSelf_ = newCell '……(3)' End Property Public Property Get oneSelf() As Range If oneSelf_ Is Nothing Then '……(4)' Call raiseError(errIndex.CELL_NOT_GOT) '……(5)' End If Set oneSelf = oneSelf_ End Property
リスト1-3の説明
珍しくProperty Set(Let)を書いた。
Javaファンの私からすると、Propertyプロシージャってなんだかヘンテコリンな感じがするので、別途Setterメソッドを作るという手もあったんですけどね。
- (1)では、このクラスに渡されるRangeオブジェクトをチェック。
- このクラスは、あくまでも単一のセルを包み込むのが目的なので、複数セルが渡されたら(2)でエラーを吐かせる。プログラマに知らせることが目的なのであえてエラーを吐かせるのだ。
エラー表示用のraiseErrorメソッドは後で実装する。 - (1)のIf節を何事もなく通過したら(3)で仮変数oneSelf_に渡されたセルをセットしている。
ちなみに、(1)のIfに対応したElse節内に書くこともできるが、あえてこうしている。
というのも、「If~Else」を多用すると可読性が落ちる(Elseの条件は、If、ElseIfと見比べないと分からない)ため。「ガード節」というらしい。たしかに「If~Else」って読みにくいときがあるんだよな。 - (4)も(1)と同じ。oneSelfプロパティにセルがセットされていないのに呼び出そうとしたら(5)でエラーを吐く。まあ、なくても普通にエラーが出るだろうけど、こうした方が原因が特定しやすいと思うので。
リスト1-4 アクセサ(2)
Public Property Get columnLetter() As String If oneSelf_ Is Nothing Then Call errorRaiser(errIndex.CELL_NOT_GOT) Exit Property End If Dim iAlpha As Integer '列符号最上位けた' Dim iBeta As Integer '列符号中位けた' Dim iRemainder As Integer '列符号最下位けた' With oneSelf_ If .Column > 702 Then '列番号が702を超えるとき、列符号は3けた '……(1)' iAlpha = Int((.Column - 27) / 676) '……(2)' columnLetter_ = Chr(iAlpha + 64) '……(3)' iBeta = Int(((.Column - (iAlpha * 676)) - 1) / 26) '……(4)' columnLetter_ = columnLetter_ & Chr(iBeta + 64) iRemainder = (.Column - (iAlpha * 676) - (iBeta * 26)) columnLetter_ = columnLetter_ & Chr(iRemainder + 64) ElseIf .Column > 26 Then '列番号が26を超えるとき、列符号は2けた '……(5)' iBeta = Int((.Column - 1) / 26) columnLetter_ = Chr(iBeta + 64) iRemainder = .Column - (iBeta * 26) columnLetter_ = columnLetter_ & Chr(iRemainder + 64) Else '列番号26までは列符号1けた '……(6)' iRemainder = .Column columnLetter_ = Chr(iRemainder + 64) End If End With columnLetter = columnLetter_ End Property
リスト1-4の説明
ココが今回のメイン。列番号の取得はめっちゃ簡単なのに、列符号の取得のなんとメンドウなことよ。高校1年程度の数学ができるんならこのコードは理解できると思う。(1)~(3)がどうしても理解できないんならあきらめてください。
- (1)。まず、1列目(A)~26列目(Z)は列符号1けた。これは楽勝ですな。んで、27列目(AA)から702行目(ZZ)が列符号2けた。
アルファベット2字の組み合わせは26×26=676通り。AAから数えて676番目のZZは、そこまでの26を足して702番目になるでしょ? - (2)は、列符号の左端の文字を求めるための計算。ZZ(702列目)の次がAAA(703列目)ってのがミソ。
703列目以降は、BAA列が(27+676)+676=1379列目、CAA列が(27+676)+676+676=2055列……というように、左端のアルファベットが進んでいくので、左端のアルファベットは[列数-27を676で割った商]番目のアルファベット、ということになる。 - 何番目のアルファベットかさえわかれば、あとは、その数字を文字に変換してやればよい。アルファベットの大文字「A」は文字コードが65番目なので、Chr関数の引数に[(2)で求めた数+64]を渡してやれば、めでたくアルファベットが得られる。
- (1)~(3)が理解できるなら、(4)はもはや説明不要だろう。
列番号から676のかたまりを除去すると、後は右の2けたの問題ですからね。
AAA列を例にとると、703-676-1=26、26÷26=1なので、真ん中のアルファベットは1番目、すなわちAということ。 - (5)は2けたになるとき、(6)は1けたになるとき、それぞれ同じような理屈で文字に変換している。
リスト1-5 コンストラクタとメソッド
'コンストラクタ Private Sub Class_Initialize() ReDim myError(2) As errorType '……(1)' With myError(0) .Number = 10000 .Description = "WrappedCellのoneSelfプロパティに複数のセルを渡すことはできません。" End With With myError(1) .Number = 10001 .Description = "WrappedCellのoneSelfプロパティにセルがセットされていません。" End With End Sub 'メソッド Private Sub raiseError(ByVal errIndex As Integer) '……(2)' Err.Raise myError(errIndex).Number, myError(errIndex).Description End Sub
リスト1-5の説明
珍しくコンストラクタの出番。このクラス独自のエラーをセットしている。
- (1)では、今のところエラーの種類は2種類だけなので、RedimしてそれぞれNumberとDescripitionをセットしている。
- (2)は、自作エラーを吐かせるメソッド。クラスの内部でしか使わないのでPrivateにしている。こんなもん、あっちこっちで使われたらかなわんしw
引数として配列のインデックス番号を受け取って、それに応じたNumberとDescriptionをセットしているだけ。
実行
標準モジュールのコード
Sub test05() Dim target As WrappedCell Set target = New WrappedCell Set target.oneSelf = Selection Debug.Print target.columnLetter Set target = Nothing End Sub
実行結果
セルを選んで、実行すると、
ほれ、この通り列符号が取得できた。
結合されたセルでも
大丈夫。
ただし、複数セルを選択して実行すると
エラーを吐く。
「[WrappedCellクラスのインスタンス].oneSelf.」まで入力すると、
このようにインテリセンスが働くので、Rangeオブジェクトのプロパティ・メソッドが普通に使える。
おわりに
とりあえず、何の役に立つのか分からないけど、今後拡張できたらしてみよう。
……とここまで書いてきてアレなんだが、
セルの列符号ぐらい、もっと簡単に取得できるんじゃね?
と思ってしまったのだった。
リスト2
Sub test06() Dim tgtCell As Range Set tgtCell = ActiveCell Dim str As String Dim chr As String Dim i As Integer Dim tmp As String str = Replace(tgtCell.Address, "$", "") For i = 1 To Len(str) chr = Mid(str, i, 1) If chr Like "[0-9]" Then chr = "" End If tmp = tmp & chr Next str = tmp Debug.Print str End Sub
リスト2の実行結果
AZN5セルを選択して実行すると、
でけとる……。
む、むなしい……orz