Ruby による Win32OLE プログラミング - Excel プログラミング(初級編)
Ruby による Win32OLE プログラミング -
Excel プログラミング(初級編)
はじめに
ここでは,前のセクションで作成した FileSystemObject クラスを使って Excel ファイルをオープンするプログラムを書いてみましょう.ただオープンするのではなく,読み取り専用で Excel ファイルをオープンするツールを作ってみます.
なぜこんなツールを作るのかという理由を少しだけ書いておきましょう.以前かかわった仕事ですが,そのプロジェクトでは開発文書が Excel ファイルとして Unix 上に大量にありました.ところが, Samba 経由で Excel ファイルを開くと何も修正していないのにファイルの更新日付が勝手に変わってしまうのです.これは困るので,急遽作ったのがここで紹介するExcelファイルを読み取り専用で開くツール xls.rb です.このツールを バッチファイル xls.bat から呼ぶようにして Meadow の dired から Excel ファイルを読み取り専用でオープンしていました.ほとんどの人には必要のないツールかもしれませんが,サンプルとして参考にしてみてください.
WIN32OLE モジュールの拡張
本題に入る前に,OLE 定数を扱うため Win32OLE モジュールを少し拡張します.Win32OLE モジュールには,OLE 定数を扱うためのメソッド const_load が用意されています.例えば,
require 'win32ole' module Excel end excel = WIN32OLE.new('Excel.Application') WIN32OLE.const_load(excel, Excel) puts Excel::XlLeft
とすれば Excel で定義されている xlLeft の値 -4131 が表示されます(Ruby では定数は大文字で始まるため Excel::xlLeft ではなく Excel::XlLeft とします).
ところが,このメソッドはちょっと使いにくいのです.というのも,もし次のように 2 回呼び出してしまった場合,const_load は Excel モジュール に同じ定数を定義しようとするため大量に警告が出てしまいます:
WIN32OLE.const_load(excel, Excel) WIN32OLE.const_load(excel, Excel)
また,WIN32OLE オブジェクトを一度生成しなければ定数を定義できないので,WIN32OLE オブジェクトを生成すると同時に定数が定義できたほうが便利です.そこで,WIN32OLE クラス に new_with_const クラスメソッドを追加します.
require 'win32ole' class WIN32OLE @const_defined = Hash.new def WIN32OLE.new_with_const(prog_id, const_name_space) result = WIN32OLE.new(prog_id) unless @const_defined[const_name_space] then WIN32OLE.const_load(result, const_name_space) @const_defined[const_name_space] = true end return result end end
このプログラムを win32ole-ext.rb ファイルとして用意します.@const_defined というクラスインスタンス変数は,二重定義を防ぐためのハッシュです.こうすると,最初の Excel::XlLeft の値を表示するプログラムは次のようになります.
require 'win32ole-ext' module Excel end excel = WIN32OLE.new_with_const('Excel.Application', Excel) puts Excel::XlLeft
二重定義を防ぐようになっているため,2回以上 同じ引数で new_with_const 呼んでも警告は起こりません.今後は,WIN32OLE#new_with_const を使って WIN32OLE オブジェクトを生成することにします.
(ファイル名は,win32ole-ext.rb ではなく win32ole.rb でもいいのですが,拡張しているということを明示的にしたかったためあえて win32ole-ext.rb としました.)
OLE ルートモジュール
Win32OLE プログラミングには,普通のオブジェクト指向プログラミングとは異なる特徴があります.
OLE で扱うオブジェクトはすべて WIN32OLE クラスのインスタンスですが,実際には単なる WIN32OLE クラスのインスタンスではありません.WSH の FileSystemObject クラスのインスタンスであったり, Excel の Application クラスのインスタンスであったりします.したがって,このような特殊な事情をうまく Ruby の言葉で翻訳する方法を考えなくてはなりません.
普通に WIN32OLE クラスから継承して 対応する OLE オブジェクトのクラスを定義する,というふうにプログラムすることができませんので,クラスの代わりにモジュールを定義し,そのモジュールに目的となる OLE オブジェクトのファクトリメソッドを持たせる,という形式にしましょう.
これを一般化して書くと次のようになります.
プログラム識別子が 'ProgID' である OLE オブジェクト `OLEObject' に対して,
require 'win32ole-ext' module OLEObject def OLEObject.new return WIN32OLE.new_with_const('ProgID', OLEObject) end endとモジュールを定義する.
このようなモジュールを OLE ルートモジュール と呼ぶことにしましょう.ルートという名前をつけた理由は,プログラム識別子で取得できる一番トップの OLE オブジェクトであるためです.OLE プログラミングでは,まず ルートオブジェクト を取得し,そこから芋づる式に OLE オブジェクトを取得していくのが特徴です.
Excel モジュール
さて,今度は OLE ルートモジュールを Excel に対して定義します.
module Excel def Excel.new(visible = true, displayAlerts = false) excel = WIN32OLE.new_with_const('Excel.Application', Excel) excel.visible = visible excel.displayAlerts = displayAlerts return excel end end
この Excel モジュールは前の節で定義した OLE ルートモジュールを少しだけ拡張しています.
Excel.Application クラスの visible プロパティは,Excel そのものを表示するか非表示にするを決定するためのプロパティです.Excel ファイルの読み込みあるいは書きこみをバッチ処理で行いたい場合,この Visible プロパティを false に設定しておくとバックグラウンドで Excel が起動します.デバッグ時に true を設定して動作を確認しておき,出来上がったら false にする,というふうにプログラムを作るといいでしょう.また,visible プロパティを false にしたほうがプログラムの実行が速くなります.
displayAlerts プロパティは,実行中に特定の警告やメッセージを表示するかどうかを表すプロパティです.この値もバッチ処理を行う場合に false にしておくと メッセージが出て処理が中断する,ということがなくなります.このプロパティも デバッグ中には true にしておき,出来上がったら false にしておく,というのがいいと思います.
RubyUnit によるテスト
このようにして定義した Excel モジュールを RubyUnit を使ってテストします.
RubyUnit に限らず Testing Framework (xUnit) 全般について言えることですが,このライブラリを拡張する一つの方向として独自の assert メソッドを作成していく,というのがあげられます.開発対象となるドメインに応じた assert メソッドを作ってライブラリ化ていくと,そのドメインのテストコードを書くのが楽になってきます.xUnit を使っている人は, assert メソッドのライブラリ化 を念頭におきながらテストコードを書いてみるとよいでしょう.
さて,ここでは assert_excel という Excel のアプリケーションオブジェクトであるかをテストする メソッドを定義します.
def assert_excel(excel) assert_equals('Microsoft Excel', excel.name, "test name property") end
assert_excel を用いた Excel#new のテストは次のようになります.
class ExcelTest < RUNIT::TestCase def setup @excel = nil end def test_new @excel = Excel.new assert_excel(@excel) end def teardown @excel.quit if @excel end end
ExcelTest が テストクラスです. setup と teardown は,それぞれテストの準備と後片付けを行うメソッドです.teardown メソッドで Excel.Application クラスの quit メソッドを呼んでいます.これを忘れると実行するたびに Excel のプロセスがどんどん作成されていくので注意しましょう.
Execute Around Method
先ほど,Excel が起動されるたびに Excel のプロセスが作成されるといいました.ちゃんと Excel を終了する処理を入れるようにしなければ,Ruby スクリプトが実行されるたびに Excel のプロセスが増えていってしまいます.終了処理を忘れずに組み込む方法はないのでしょうか?
ここでは,自動的に Excel を quit する仕組みを考えることにしましょう.
C++ などでは,デストラクタを用意しそこで Excel を終了させる,という方法があります.しかし,ここではデストラクタのような方法ではなく,Execute Around Method(以下 EAM と略します)という パターンを使うことにします.EAM は,Kent Beck が 彼の本 Smalltalk Best Practice Patterns の中で挙げているコーディングパターンの一つです.
このパターンは,あるコードに対して前処理と後処理が必ず必要な場合に用いられます.Excel プログラミングでは,Excel の起動と終了という二つの処理が必要ですので,その二つの処理を自動的に行う EAM を定義します:
module Excel def Excel.runDuring(visible = true, displayAlerts = false, &block) begin excel = new(visible, displayAlerts) block.call(excel) ensure excel.quit end end end
こうすると,例えランタイムエラーが起こったとしても Excel が終了します.Ruby を使っていてうれしいことの一つとして,例外機構がちゃんと用意されていることが挙げられますね.`runDuring' というネーミングは,Kent Beck の ネーミングルールに従いました(EAM には xxxDuring という名前を付けます).
例によって,これを使ったサンプルを RubyUnit によるテストコードで挙げておきます.
def test_runDuring Excel.runDuring do |excel| assert_excel(excel) end end
このように, Excel#runDuring に渡すブロックの中で WIN32OLE オブジェクトである excel を使って Excel を操作します.
先ほど言ったように,デバック時は勝手に Excel が終了されると確認できないため,displayAlerts 引数を true にしておきます.例えば
Excel.runDuring do |excel| doc = excel.workbooks.add doc.worksheets(1).cells(1,1).value = 'Ruby' end
とすると,本当に シートに Ruby という文字が書き込まれたかどうか速すぎて判断できません.そこで,一時的に
Excel.runDuring(true, true) do |excel| doc = excel.workbooks.add doc.worksheets(1).cells(1,1).value = 'Ruby' end
と Excel#runDuring に渡す第二引数 (displayAlerts) を true にします.こうすると `Book1への変更を保存しますか?' というダイアログボックスが表示されるようになるので,キャンセルボタンを押してシートの内容をゆっくりと確認することができます.
xls.rb スクリプト
さて,以上で Excel モジュールの作成が終わりました.このモジュールを excel.rb という名前のファイルで定義することにしましょう.ここまでくれば,Excel ファイルを読み取り専用で開くツール: xls.rb は簡単に作成できます.
require 'excel' require 'file-win32' filename = FileSystemObject.instance.getAbsolutePathName(ARGV[0]) excel = Excel.new excel.workbooks.open({'filename'=> filename, 'readOnly' => true})
最後の一行が Excel ファイルをオープンするメソッドです.このプログラムではオープンするだけで終了する必要はないので Excel#runDuring メソッドを使いませんでした.また,Visual Basic の名前付き引数渡しは Ruby ではハッシュを使って実現しているところにも注意してください.
(なお,Excel モジュールのテストコードも exceltest.rb として用意しました.)
補足
OLE ルートモジュールは,Excel に限らず OLE 対応のアプリケーションならどんなものでも使えます.例えば Visio の場合は次のようになります:
module Visio def Visio.new(visible = true) app = WIN32OLE.new_with_const('Visio.Application', Visio) app.visible = visible return app end def Visio.runDuring(visible = true, &block) begin app = new(visible) block.call(app) ensure app.quit end end end
上のコードを一般化して,OLE のアプリケーションクラスすべてに対して使えるモジュールを定義した方がよいのかもしれませんが,そうするまでには至っていません.OLE ルートオブジェクトは,visible プロパティを持っているかどうかわからないし, quit メソッドも各アプリケーションによって引数をとったりと仕様がまちまちだからです.
また,Microsoft Project などのように,一度に起動できるアプリケーションは一つだけ,というものもあります.このようなアプリでは,プログラム側で勝手に quit してしまうと例えそのときにユーザが別の作業で使っていてもそのアプリは quit されてしまいます.こういった場合は,runDuring のような メソッドを用意せず,スクリプト内で 勝手に quit しないようにするほうがよいでしょう.一つのプロセスが生き残ったままになるかもしれませんが,このタイプのアプリについてはしょうがないでしょう.