Excelオブジェクト

MS-Excelは,ワークブックからセルの条件付き書式までExcelのあらゆる構成要素をVBAから管理,操作するためのオブジェクトモデルを提供しています。これをExcelオブジェクトモデルと呼びます。開発環境(VBE)のプロジェクトエクスプローラーには,各プロジェクトの下にMicrosoft Excel Objectsというグループがあり,ワークブック,ワークシートなどに対応するオブジェクトモジュールが表示されます。
VBE Sheet1オブジェクトモジュールの画面
プロジェクトはワークブックごとに割り当てられ,Excelオブジェクトモデルは現実のワークブックの構成を反映します。たとえば,Excelの画面からマニュアル操作でワークシートを追加すると対応するワークシートオブジェクトが自動的に用意され,ワークシートを削除すると対応するオブジェクトも消滅します。

Excelオブジェクトはいつでも使用可能です。たとえば,Excelのウィンドウで1枚目のワークシート(上の例ではシート見出し"Q1")が表示されているときにイミディエイトウィンドウから次のステートメントを実行すれば,2枚目のワークシート(上の例ではシート見出し"Q2")が前面に出てきます。これは,Sheet2が2枚目のワークシート"Q2"に対応するオブジェクトで,Selectがそのワークシートにフォーカスを移すメソッドだからです。
Sheet2.Select

また,次のステートメントは1枚目のワークシートのシート見出しをイミディエイトウィンドウに出力します。
Debug.Print Sheet1.Name

VBEのコードエディタでオブジェクトモジュールにユーザー定義プロシージャを編集します。このとき,エディタ上部の2つのリストボックスからオブジェクトとイベントを選択することでイベントハンドラを自動で追加することができます。次のコードでは,ワークシートオブジェクトのSheet1モジュールにWorksheet_Activeイベントハンドラとユーザー定義のPropertyプロシージャReportTypeを追加しています。

Private Sub Worksheet_Activate()
    Msgbox "This sheet was activated."
End Sub

Public Property Get ReportType() As String
    ReportType = "Sales Result"
End Property
Sheet1オブジェクトモジュールにユーザーコードを追加

1~3行目のイベントハンドラは,Excel本体においてこのシート名"Q1"のワークシートにフォーカスが移るたびに呼び出され,メッセージボックスを表示します。5~7行目のPropertyプロシージャは別のコードから呼び出して使います。たとえばイミディエイトウィンドウで次のステートメントを実行すると,"Sales Result"という文字列を出力します。
Debug.Print Sheet1.ReportType

一見すると,モジュールに追加したプロシージャがこのように使用できるようになるのは,当然のように思えます。しかし,一般的なクラスとインスタンスとの関係に当てはめてみると,実はいくつか不思議な点があります。一点目は,そもそもSheet1とは何かということです。Sheet1オブジェクトモジュールを編集してプロシージャを追加しているのだから,ここでのSheet1とはクラスだと考えられます。一方,Sheet1.ReportTypeとしてプロパティが取得できるということは,ここでのSheet1は必然的にオブジェクト参照です。つまり一概にSheet1といっても,状況によって型であるクラスと実体であるオブジェクトという異質のものを指しているのです。

もう一つは,Sheet2.SelectのSelectメソッドやSheet1.NameのNameプロパティなどは,それぞれのモジュールでは定義されていないのになぜ使用できるのかということです。

2点とも,Excelが特別にそういう仕組みを提供しているのだ,の一言で片づけることもできますが,もう少し掘り下げてExcelオブジェクトの実装が,VBAの言語体系の枠組みの中でどのように実現されているのかについて考えてみたいと思います。

Excelオブジェクトの解釈

Sheet1クラスとSheet1オブジェクトについては,両者を別物と考えれば説明がつきます。VBAではクラス名と同じ変数名を定義することができるので,Excel側はSheet2型のグローバルオブジェクト参照変数Sheet2を用意しておき,常にこの変数がワークシートSheet2に対応するオブジェクトを参照するようにしておけばよいのです。

これについて,次のようにすると,Excelオブジェクトを管理するためにExcelが内部で何をしているかを垣間見ることができます。

まず,次のコードをSheet2モジュールに追加します。ObjPtrは,アンサポート関数で,オブジェクトが置かれているメモリ上のアドレスを返します。

Private m_Dummy As Integer

Public Sub PrintPointer()
    Debug.Print " ObjPtr of Me: " & Hex(ObjPtr(Me))
End Sub
Excelオブジェクトの挙動を調べるためのテストコード(Sheet2モジュール)
次に,イミディエイトウィンドウから以下のようにPrintPointerプロシージャを実行すると,現在のSheet2オブジェクトが存在するメモリ上のアドレスが表示されます。
Sheet2.PrintPointer
続いて,Sheet2モジュールの1行目を削除してからもう一度上記のステートメントを実行します。すると,今度は最初とは違うアドレスが表示されるはずです。
Sheet2.PrintPointer
 ObjPtr of Me: 70BCAF0
Sheet2.PrintPointer
 ObjPtr of Me: 70BB980
つまり,Sheet2は常にワークシートSheet2に対応するオブジェクトを参照していますが,参照先は必要に応じて入れ替わっているのです。削除した1行目はクラスのメンバー変数定義でした。メンバー変数の定義が変わればオブジェクトのデータ構造が変化するので,以前に存在したオブジェクトは無効になり,新たなオブジェクトを作り直すしかないのです。

実は,まったく同じ仕組みをユーザー定義クラスで提供することもできます。ユーザー定義クラスに上記のコードを追加してポインタ表示後,変数宣言部分を削除してから再度ポインタを表示します。もちろんユーザー定義クラスの場合はオブジェクトが自動的に生成されないので,標準モジュールでオブジェクト変数を明示的に宣言しておく必要があります。下のようにNewキーワード付きなら必要に応じてオブジェクトが自動生成されます。
1:
Public g_Object As New MyClass
ユーザー定義クラスMyClassのオブジェクト宣言(標準モジュール)
定義されたMyClassクラス型オブジェクト参照g_Objectは,ワークブックが開かれている限りいつでも使用可能になります。

Excelオブジェクトがこのようなグローバルオブジェクトと違うのは,オブジェクトが対応付けられた「本尊」(ここでの例ではSheet2ワークシート)に同期して生成・解放されるという点だけです。(※補足

さて,Excelオブジェクトといえども通常のクラスオブジェクトと本質的な差はないことが分かってきたところで,次のプロシージャを試してみましょう。Sheet1モジュールがSheet1クラスの定義であるならば,Sheet1型のオブジェクト変数を定義することも,Sheet1型の新しいオブジェクトを作ることもできるはずです。

Public Sub TestSheetObject1()
    Dim sh As Sheet1
    Set sh = Sheet1
    Debug.Print sh.Name
End Sub

Public Sub TestSheetObject2() 
    Dim sh As Sheet1
    Set sh = New Sheet1
End Sub
Sheet1オブジェクト検証のためのテストコード(標準モジュール)
TestSheetObject1を実行すると1枚目のワークシートのシート見出しが表示されます。2行目でSheet1(クラス)型変数shを定義し,3行目でSheet1(オブジェクト参照)の値をセットしています。この時点でshはオブジェクト参照Sheet1と同じオブジェクトを参照しているので,4行目はSheet1.Nameと同じ意味になるのです。

TestSheetObject2は,既存のオブジェクト参照Sheet1をコピーする代わりに新たなSheet1型オブジェクトを生成しようとしたものです。が,これは9行目が「Newキーワードの使用法が不正です。」というコンパイルエラーになります。これは,Excelオブジェクトが参照設定された別プロジェクト(Microsoft Excel Object Library)で定義されていることが理由で,VBAの仕様としてプロジェクト外から生成済みのオブジェクトにアクセスすることはできても,新規オブジェクトの生成はできないのです。もっとも同じワークブックに対応するオブジェクトをいくつも作られてもシステムとしては管理に困るでしょう。

もう一つ,定義されていないSelectやNameといったメンバーメソッドやメンバープロパティがなぜSheet1オブジェクトやSheet2オブジェクトで使用できるのかという疑問を検証するために以下のコードを試してみましょう。

Public Sub TestSheetObject()
    Dim sh As Worksheet
    Set sh = Sheet1
    Debug.Print sh.Name
    Set sh = Sheet2
    Debug.Print sh.Name
End Sub
Sheet1オブジェクトをWorksheet型オブジェクト参照で受けたテストコード(標準モジュール)

このプロシージャを実行するとSheet1とSheet2のNameプロパティが出力されます。つまり,Worksheet型オブジェクト参照のshは,Sheet1型オブジェクトもSheet2型オブジェクトも参照することができるのです。これは,まさにSheet1クラスやSheet2クラスがWorksheetクラスから派生している場合の振る舞いです。VBAの言語使用としては継承はサポートされていませんが,それはユーザー定義クラスについての話で,システムが提供するExcelオブジェクトモデルにおいては散見されるようです。少なくともそのように理解すると,実装されていないはずのメソッドやプロパティがSheet1クラスやSheet2クラスのオブジェクトについて使用できることの説明がつきます。つまり,それらは基底クラスであるWorksheetクラスで定義されているのです。実際にVBEのオブジェクトブラウザで確認すると,WorksheetクラスのすべてのメンバーメソッドやメンバープロパティがSheet1やSheet2等の個別ワークシートに対応するクラスでも使用可能になっていることがわかります。
VBEオブジェクトブラウザのイメージ
同様の継承は,他のオブジェクトクラスにおいても見られます。主なところは次の通りです。

Excelオブジェクト 基底クラス名 派生クラス名
ワークブック Workbook ThisWorkbook
ワークシート Worksheet Sheet1,Sheet2,...
グラフシート Chart Graph1,Graph2,...
ツールバーコマンド,
ショートカットメニュー
CommandBarControl CommandBarButton
CommandBarComoboBox
CommandBarPopup

まとめ

以上をまとめると次の通りです。
  • Microsoft Excel Objectsに表示されているのは,Excelオブジェクトに対応するクラス名であると同時に,唯一のインスタンスを参照するオブジェクト変数名でもある。
  • Excelオブジェクトクラスは派生クラスであり,その基底クラスであるWorksheetクラスやWorkbookクラスが同種のオブジェクトに共通の機能を実装する。
Excelオブジェクトモデルは,難しい概念を意識しなくても誰にでも簡単にアプリケーションを作ることができる優れた仕組みを提供しています。しかも,その実装は,クラスの継承のような機能拡張を用いつつも本来のVBAの言語仕様からは外れずに自然な形で行われているのです。
web拍手 by FC2
inserted by FC2 system