04/02/29 | CodecをつかったMP3のデコード・再生編−2(MP3のフレームヘッダ解析)[★★★◎◎] | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
前回(と言っても1年近く前だな・・・)では、MP3ファイルからID3データなどを除去し、MP3の本当に再生に関係する生データの部分だけを取り出すことができました。 前回書いた通り、Codecを用いるためのACM(Audio Compression Manager)系のAPIはWAVEFORMAT構造体を拡張したWAVEFORMATEX構造体を送る必要があります。 WAV形式のMP3ファイルの場合は、WAVファイルのヘッダの中にWAVEFORMATEXが含まれているので問題ありませんが、通常のMP3ファイルの場合は中を自分で見てWAVEFORMATEX構造体の各値をセットする必要があります。 MP3ファイルの大まかな構造については 最初から説明するInside MP3 に詳しい説明があるので、細かく知りたい方はそちらを見たほうがいいかと思います。 ここではVBでデコードするのに必要な最低限の情報に関して触れて行きます。 あらかじめ触れておきますが、ここで書いてある情報は可変ビットレート(VBR)には対応していないので注意してください。 以後固定ビットレート(CBR)に限定して書いて行きます。 フレーム MP3のデータは、フレームと言うデータの塊が並んだ形をとっています。 各フレームは1152サンプル分の情報を含みます。 よくある44100Hzでサンプリングしているデータの場合、1秒当たり40フレーム弱あることになります。 固定ビットレートの場合は各フレームはほぼ同じサイズになります。(数バイト程度の差しかない) ここで重要なのは、各フレームは完全に固定したサイズを持っていないことです。 また、MP3ファイル中に「フレームはいくつある」とか「各フレームは何バイト目から始まる」という情報はありません。 そのため、先頭のフレームから1フレームずつ長さを計算して順番に位置を求めて行く必要があります。 各フレームの先頭4バイト=32ビットがフレームの属性を表すフレームヘッダになります。 今回デコードするのに必要な項目についてだけ触れて行きます。 この32ビットは以下のような構成になります。 (サイトによって微妙に異なる記述がされているようなので、実際に利用する時は注意してください) 他のサイトでは以下のサイトにも情報があります。 最初から説明するInside MP3 The Programmer's File Format Collection
これで最初の4バイトからバージョン・レイヤー・ビットレート・サンプリングレートが取得できることになります。 ここから、以下の様にフレームサイズが計算できます。 Paddingは上の値の通り0か1です。
また困ったことに、サイトによってはLayerIをIIやIIIと同じ式にしているところがあります・・・ LayerIでは各フレームが4バイト境界を持たなければいけないため4の倍数になるようになっています。 それでもほぼ48*BitRate/SampleRateのサイズということでLayerII・IIIの1/3になっていますが、LayerII・IIIでは1152サンプルが含まれるのに対し、LayerIでは384サンプルが含まれていることによるようです。 後者の式でパディングを除いて考えた場合、なぜこの式でいいかを少し考えてみます。 (1フレームの秒数)=1152 / SampleRate (1フレームのバイト数)=(1フレームの秒数) × BitRate / 8(ビット→バイト) = 144 * BitRate / Samplerate よって確かに正しいと言えます。 参考までに、Mpeg1、LayerIIIの場合のパディングを除いたバイト数を列挙しておきます。 サンプリングレートが上がると1フレーム当たりの秒数が下がるので、使えるバイト数も減りますね。
これを見て分かる通り、よく使われる44KHz・128Kbpsだとほぼ1/10に圧縮されることが分かります。 実際のフレームデータを見てみると、パディングがあるフレームとないフレームがばらついています。 それで全体として指定したビットレートにあうようにサイズ調整がされます。 これで、サンプリングレートやフレームサイズがわかったため、WAVEFORMATEXも埋めることが出来ますし、1つずつフレームをたどることが出来ます。 実際にByte配列のbuf(Index)から始まるフレームデータからWAVEFORMATEX(MPEGLAYER3WAVEFORMAT)を設定する関数は↓みたくなります。
今回は短めですがここらへんで。(って↓のアウトラインの話が長すぎなんだな・・・) 次回に実際にデコードしてみます。 |
03/12/05 | 文字のアウトライン取得・操作編[★★★◎◎◎] | ||||||||||||||||||||||||||||||
最近Flashを使ったアニメーション作品をよく見かけるが、たまに文字のモーフィングを行っている。 これを行うのに必要な「文字を構成している頂点や線分の取得」について調べてみた。 こういう用途にはGetGlyphOutlineというAPIが利用できる。 このAPIは何気に役に立つ。 1つはアンチエイリアスをかけた状態の文字のビットマップデータを返してくれること。 アンチエイリアスも5段階、17段階、65段階と様々な段階に対応して返してくれるので、DirectXでテクスチャとして扱ったりする際も役に立つと思われる。 しかも2x2行列を指定すると線形変換をした結果を出力するので、拡大縮小、回転も行える便利な関数である。 実際GetGlyphOutline関数で検索すると、アンチエイリアスをかける目的のプログラムの方が多くヒットする。 まぁそれはそれで置いておいて、今回はフォントの構成要素を取得する機能の方を使うやり方を調べてみた。 正確には、GetGlyphOutlineはフォントの輪郭を取得することができる。 当然ながら、ラスタフォント(ビットマップがそのまま入ってる奴。SystemとかTeminalみたいなの)は今回は対象外。 TrueTypeフォントが対象となる。 実際の動くサンプルがその45−文字のアウトライン応用サンプルにある。 TrueTypeフォントの構成 簡潔にいえば、WindowsのTrueTypeフォントは、線分と2次ベジェ曲線で構成される。 (2次Bスプラインと記述しているところもあるが、2次ベジェ曲線は2次Bスプラインで特定のパラメータを与えた場合なので間違いではない。) 各文字は、複数のブロック(この単語は適切かどうかわからない・・・SDKにはseries of polylines and splinesとある)からなり、各ブロックは、複数のカーブ(SDKにはレコードと記述してある)からなる。 当然ながら、各カーブは複数の制御点によって作られる。 次の図を見るとわかると思う。 左側は同じブロック同士を同じ色で描いた「あ」である。 「あ」は黒、紫、緑の3つのブロックからなる。 見てわかるように「輪郭」と言っても外側と内側があるので、中を塗ろうとすると面倒。 ともあれ、各ブロックはループ状になっていることがわかる。 このループをさらに各カーブごとに色分けしたものが真中である。 ひらがななせいもあってほとんどベジェ曲線要素だが、1画目のあたりの赤線のところは2・3回曲がっている。 直線が連続しているところは1つのレコードで一気に描いてしまうが、曲線部はこまごま分かれていることがわかる。 そのため、カクカクした漢字はレコード数が少ない。 一番右は全角の0(ゼロ)だが、内側と外側の2ブロックからなり、各ブロックは8つのベジェ曲線からなっている。 これは0に限らず、O(オー)とかpとか8とか円形のものはだいたい8分割で1周になりそうだ。 各ブロック・カーブの取得 ようやくGetGlyphOutlineでブロック・カーブを取得する。 VBだと多少面倒な処理があるので、下の文を読むのは面倒とか、さっさと点だけ取りたい場合はサンプルにある関数を使うと下の処理を行ってくれる。 その場合下の点の形式まで読み飛ばしてもらって構わない。 ただ、ここでVBでは少し問題がある。 APIで可変長の配列を対象とする場合、以下のような表記がされる。 例えば、BITMAPファイルにあるBITMAPINFO構造体や、今回の1つのカーブを表現するTTPOLYCURVE構造体は以下のように記述される。
BITMAPINFO構造体のRGBQUADはパレットが入るが、パレットの色数は4bitなら16個だったり、8bitだったら256個だったりするので、実際に必要なメモリサイズは BITMAPINFO *bi=(BITMAPINFO*)malloc(sizeof(BITMAPINFO) + sizeof(RGBQUAD) * (nColor - 1)); みたいな感じで扱う。 当然VBではポインタとかキャストとかを扱うことはできないし、動的配列も↑の様にbmiHeaderの後にbmiColorsの配列がいくつも続くようにはとれない。 (bmiColorsを動的配列で定義した場合、中身が別の場所に確保されて、単にbmiHeaderの後にはポインタだけが格納される) ここらへんはあとで適当にバイト列をこまごまコピーとかしてどうにかするとして、実際の取得に入る。 ((2005/11) cpBufferとlpvBufferの説明を修正)
とりあえず、アウトライン情報が何バイトあるかわからないので、一端cpBufferのところをvbNullCharかByVal 0としてNULLを送り、返り値のサイズでバイト配列を確保して再度取得すればよい。 簡単に書くと下みたいな感じ。
ここで、バイト配列Bufの中にアウトラインの情報が入るので、次にこの情報を対応する構造体に格納することにする。 上記の通り、1つのブロックに複数のカーブ、1つのカーブに複数の制御点と、可変長の配列が二重になっているので少し面倒。 ちなみに、MFCマニュアルのCDC::GetGlyphOutlineには、以下のように記述されている。
正直、この文章だけで構造がわかる人は少ないと思う・・・ と思ったら、自分で解析後、 Microsoft Knowledge Base Article - 87115 HOWTO: GetGlyphOutline() Native Buffer Format をみつけたんだが・・・ まぁ上のも英語だしC言語だしわかりにくいので解析結果を書いて行きます。 データ構造は以下の通り。
とこのような感じになっている。 可変長の部分の処理が面倒なので、サンプルでは、GetGlyphOutlineを呼んでアウトライン・ブロック・カーブの構造を表す下のような構造体を取得する関数を作成している。
((2005/11) 以下を追加) なお、得られる点の座標はフォント指定(CreateFont API呼び出し)時のHeight・Widthのサイズと、GetGlyphOutline時に指定した2x2行列の積で決定される。 ここでHeight・Widthを1にしておき、行列も単位行列にしておくとすべての点が(0,0)-(1,1)に収まって扱いやすいはず。 と思ったが、なぜか返り値が0〜1程度に小さくなるようなHeight・Width・行列指定にしたところ、うちの環境では一部の点の座標が変な値が返って来た。 これらの値を少しずつ大きくしていくと座標のずれが直ってきた。 ということでHeight・Widthは大き目(せめて16ぐらい?)にして、返って来た座標をHeight・Widthで割って(0,0)-(1,1)に収めた方がいいかも。 うち以外の環境でも発生するかはわかりませんが… 点の形式 で、結局GetGlyphOutlineでなんか点を色々読みこんだが、一体どう言う意味があるのかわからなければどうしようもない。 各直線・曲線については後述するとして、各点の意味を見てみる。 まず、各点は(0,0)〜(1,1)の範囲の座標を持っている。 そのため、もしサイズが100x100の文字にしたいのであれば、各点を100倍すればよい。 この点の座標は、POINTFXと言う固定小数点による構造体からなっている。 (WinAPIは基本的に小数もムリヤリ整数で扱いたいみたいだね。)
まぁPOINTFXは2次元座標を表すのでxとyがあるのはいいとして、問題はFIXED構造体。 これは整数部分と小数部分をそれぞれIntegerで持っている。 整数部分はいいとして、小数部分は65536(=2^16)を分母にしたときの値を持っている。 要は普通の小数に直したい場合は Value + fract / (2^16) をとればよい。 ただ、問題としてVBのIntegerは符号付で-32768〜32767の値を持つため、以下のような関数FIXEDをSingle型に変換している。
これでFIXEDをSingle型に変換できる。 なお、サンプルでは取得した点をの座標をPOINTFXとFIXEDで持つのではなく、Single型x、yの値を持つPointSingleと言う型を定義して保持している。 これでSingle型に置換できたが、もう一つ問題がある。 この点の座標は数学の座標に基づいてつけられている。 つまり、Yの値が大きいほど上にあることをあらわすため、コンピュータの画面の座標と向きが伴う。 ただ、これは y = 1 - y で上下逆にすればよい。(100倍に拡大とかしたいなら、拡大前にこの処理を行うか、拡大後 y = 100 - yで上下反転させなければならない) アウトラインの描画 ここまでやると点の座標も無事取ることができたことになる。 後は好きなように点を揺らしてみたり変形したりすればいいことになるが、最後にはまた描画したくなる場合があるため、描画について考えてみる。 各ブロックは独立しているので、個々のブロックの描画をすればよい。 個々のブロックを描画するには当然ブロック内の各カーブを描画すればよい。 各カーブは、直前のカーブの最後の制御点と自分の持つ制御点を用いて描画する。 最初のカーブは「直前のカーブ」が存在しないため、かわりにブロックのヘッダであるTTPOLYHEADERのpfxStartを始点として用いる。 (もしサンプル内の構造体を使うのであればFOBlock.StartPoint) また、最後には、最後のカーブの最後の制御点とこの始点を直線で結び、1周描きおわったことになる。 各カーブは折れ線またはスプライン曲線である。 折れ線の場合は非常に簡単に処理できる。 直前のカーブの最後の制御点がP、描画対象のカーブの持つ制御点がA、B、C、・・・とあるのであれば、単純にP-A-B-C-・・・と直線で結んで行けばよい。 この場合PolyLine APIを使うと簡単に処理できる。 スプライン曲線の描画(2次ベジェ曲線の描画) 問題はスプライン曲線である。 まずは簡単なカーブの持つ制御点が2個の場合を考えて行く。 直前のカーブの最後の制御点がP、対象カーブの持つ制御点がA、Bの合計3個だった場合、制御点P、A、Bからなる2次ベジェ曲線を描けばよい。 この2次曲線を描く場合には、自分で点を計算して行くか、APIのPolyBezier関数を用いるの2通りがあるため、それぞれについて触れて行く。 ◆自分で点を計算して行く 制御点をP、A、Bからなる2次ベジェ曲線C(t)は次の式で表される。 C(t) = (1-t)2 P + 2(1-t)t A + t2 B ここで、tを0→1まで変化させて行くと順に曲線上の点が得られるため、各点を直線で結んで行く。 点を取る間隔を変化させれば当然滑らかさが変わる。 実際にはP、A、Bの3点間の距離で曲線の長さはだいたい見当がつくと思うので、そこそこ滑らかに見えるように5〜30分割程度して行けばいいと思われる。 この手法は滑らかさを考慮したりしないといけないが、一方「アウトラインの曲線をポリゴン化したい」とか言う場合には曲線上の点が任意の精度で取得できるので便利である。 下のPolyBezierを用いる手法は描画しか行えない。 ◆PolyBezier APIを利用する Win32APIでは、4つの制御点を指定して3次ベジェ曲線を描画するPolyBezierという関数があるため、これを用いる。 当然、「描きたい曲線は制御点3つ、2次の曲線で、PolyBezierは制御点4つ、3次の曲線だけど・・・?」 となるので、ベジェ曲線の次数上げと言う手法を用いる。 具体的には、元の制御点P、A、Bを以下の計算でP、A'、A''、Bの4点にし、PolyBezier関数に渡せばよい。 A' = 1/3 * P + 2/3 * A A'' = 2/3 * A + 1/3 * B 図にすると下の感じ。 なんでこれが使えるのが知りたい場合は、最後に補足でベジェ曲線の次数上げについて書いておく。 手っ取り早く確認したい人は、 C(t) = (1-t)2 P + 2(1-t)t A + t2 B = (1-t)3 P + 3(1-t)2t A' + 3(1-t)t2 A'' + t3 B が成り立つことを計算してみるとよい。 簡単に言えば「2次曲線も3次曲線の一部(3次の係数が0になっただけ)」ということになる。 これをベースに、制御点が3つ以上ある場合を見て行く。 スプライン曲線の描画(制御点が3つ以上の場合) MSDNとかみても制御点が3つの場合しか触れていないが、一部フォントでは制御点が4つ以上になる場合がある。 面倒なのでここでは最初から「3つ以上」の場合を扱うことにする。 4つ以上の場合に関しては「とりあえずこっちでテストした感じあってるっぽい」というだけなので確認した方がいいかも。 例えば制御点が直前のカーブの最後の制御点Pを含めたP、A、B、C、D、Eとなったとする。 まず、最初の2個と最後の2個を除いて、隣同士の点の中点を取る。 例えば点Aと点Bの中点をMABと表すことにすると、 (P A B C D E) → (P A MAB B MBC C MCD D E) と点が増える。ここで、点を3つずつとり、上記の2次ベジェ曲線の描画をそれぞれ行えばよい。 ただし、やはり各ベジェ曲線の終点は次の曲線の始点と共有するので、結局 (P A MAB) 、 (MAB B MBC) 、 (MBC C MCD) 、 (MCD D E) を描画することになる。 図にすると下の感じ。 この描画手法を見てわかるように、この曲線は各ベジェ曲線の始点・終点であるP、MAB、MBC、MCD、Eを通るが、A、B、C、Dは通らない。 すなわち、「カーブ全体の始点P・終点Fは通る」「各中点Mは通る」「両端以外の制御点A、B、C、Dは通らない」ことがわかる。 なぜこんな描画方法で、こんな性質になるかを推測してみたので、スプライン曲線解析で触れてみた。 推測だけど。 PolyBezierは連続する3次ベジェ曲線をまとめて描いてくれるので、次数上げと合わせた処理として以下のような点配列を渡すのも可。 個々のベジェ曲線の端点(P(0)=P、P(3)=MAB、P(6)=MBC、P(9)=MCD、P(12)=E)は共有されるので、計13個の点をPolyBezierに渡すことになる。 P(0) = P P(1) = 1/3*P + 2/3*A P(2) = 2/3*A + 1/3*MAB = 5/6*A + 1/6*B P(3) = MAB = 1/2*A + 1/2*B P(4) = 1/3*MAB + 2/3*B = 1/6*A + 5/6*B P(5) = 2/3*B + 1/3*MBC = 5/6*B + 1/6*C P(6) = MBC = 1/2*B + 1/2*C ・ ・ ・ P(11) = 2/3*D + 1/3*E P(12) = E まとめ これでアウトラインの点を取得し、描画することができた。 これだけできると、↓のようにちょっとしたプログラムが作れる。 元々左側のようなゴシック体の「龍」に対し、制御点がランダムに動く。 さらにマウスをクリックすると近くにある制御点が遠ざかり、右のように変形される。 ここまででは輪郭を線で扱っているだけだが、これをポリゴンで扱おうとするとかなり面倒である。 (「立」の部分のように凹な部分があったり、穴が開いてたりするので三角形に分割するのが難しい) もし興味がある人はそこらへんまでやってみると、Flashの文字モーフィングみたいなことができると思う。 以下補足なので興味のある人だけ。 補足1:ベジェ曲線の次数上げ p0n 〜 pnnのn+1個の制御点からなるn次ベジェ曲線があった場合、そこから同じ曲線を表現するp0n+1 〜 pn+1n+1のn+2個の制御点からなるn+1次ベジェ曲線を作成することができる。 (上の沿え字は「n乗」ではなく単なる沿え字) 計算方法は以下の通り。 pin+1 = i / (n+1) pi-1n + (1 - i / (n+1)) pin 上の2次→3次の式でも触れたが、値を実際に計算してみると、n+1次曲線のほうでもn+1次の項が0になるのがわかる。 補足2:スプライン曲線解析 上では制御点が増えた時はなぜ中点を取ってベジェ曲線で結べばいいのか。 SDKをみても、「制御点が3つの場合、直前の点PとあわせてP、A、B、Cとなるが、AとBの中点Bをとり、(P A M)と(M B C)をベジェ曲線で描けばよい」としか書いていない。 そもそもなんで中点が出てくるのかを推測してみる。 スプライン曲線にはノットというパラメータがあるが、この値が 「始点及び終点は同じ値を2つ持つ、それ以外では1だけ異なる値をもつ」となっているのではないだろうか? とすると、上記のP、A、B、C、D、Eの例では、P(0,0)、A(0,1)、B(1,2)、C(2,3)、D(3,4)、E(4,4)となる。 この場合各中点MAB、MBC、MCDのノットはMAB(1,1)、MBC(2,2)、MCD(3,3)となる。(下図みたいな感じ) さらに描画すべき各曲線は(P(0,0) A(0,1) MAB(1,1)) 、 (MAB(1,1) B(1,2) MBC(2,2)) 、 (MBC(2,2) C(2,3) MCD(3,3)) 、 (MCD(3,3) D(3,4) E(4,4))となる。 この場合、「ここの曲線はベジェ曲線」「始点P・終点E及び各中点MAB、MBC、MCDは通過するが、A、B、Cは通過しない」という性質にぴったり合致する。 推測ではあるが、大体あっていると思う。 |
03/11/25 | IEのコンテキストメニュー拡張編[★★★◎◎] | ||||||||||||||||||||||||||||
なんだかんだでWindowsはシェアも多いし、Internet Explorerもシェアが多い。 というわけで、IEまたはタブブラウザなどのIE派生ブラウザを使用している人は多いと思う。 一部のソフトでは、ブラウザで右クリックしてでるメニュー(コンテキストメニュー)と連携しているものがある。 有名な例ではirvineがある。 ここに自分で作ったプログラムを登録、利用する方法を調べてみた。 (作成したサンプル群はプログラム部屋のその43−IEのコンテキストメニュー拡張サンプルに載せてあります。 メニューの登録方法 まずはメニューに追加する方法を調べる必要がある。 これは結構使われているテクニックなこともあり、Googleで「IE 右クリック 追加」等で検索するといくつか紹介しているサイトが見つかる。 例えば、林道の鬼の「VBのコーナー」→「IEの右クリックメニューに項目を追加」に詳しく書いてある。 また、マイクロソフトのサイトでも[InetSDK] ブラウザの標準コンテキスト メニューに項目を追加するという情報が公開されている。 要約すると以下の通り。 レジストリの[ HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\MenuExt ]の下にキーを作る。 (MenuExtがない場合はMenuExtも自分で作る。) キーの名前はそのまま右クリック時のメニューの名前。 標準の値は呼び出すファイル名、Contextsはメニューに表示される条件、Flagはスクリプトをロードしたウインドウの処理。 Flagはなくてもよいが、後は必要。 Contextsはメニューの表示される条件だが、
のようになっている。 例えば通常の右クリック時とテキストを選択して右クリックした場合に表示したい場合は、0x1 OR 0x10 = 0x11となる。 レジストリを直接扱ってもいいし、もし配布したりすることを考えたら拡張子regのファイルを準備しておいて、
等と書いておくと、テキストを選択して右クリックを押すと「半角変換」というメニューが追加された状態になる。 クリックするとc:\sample\hankaku.html内のスクリプトを実行する。(パスの区切りが\\なのに注意) ファイル名さえ書き換えてもらえるならhtmlファイルとregファイルを一緒にすれば、作ったものを配布できるでしょう。 このメニューの追加や削除のためのレジストリ操作を行ってくれるソフトにIE MenuExt(うりゅそふと)等のソフトが利用できます。 呼び出されるスクリプト で、上述のファイル名の拡張子は.htmlになっています。 この理由として、このスクリプト起動の仕組みを見る必要がありますが、後述と言うことでとりあえずスクリプトを書いてみます。 html内なので、通常使えるのはJScript、VBScriptとなります。 メニューを呼び出す前のページの情報が欲しい場合は、external.menuArguments.windowやexternal.menuArgument.documentで取得することが出来ます。 これらのdocumentやwindowオブジェクトに対して行える操作はとほほのWWW入門などで確認できると思います。 例えば、簡単なサンプルとして
とすると呼出元ページのURLとタイトルが表示されます。 (しかしこれ書いててHTMLソースを色づけして再度HTMLにするプログラム欲しくなってきた・・・) もうちょい頑張ると色々できます。 下のものは、テキストを選択した状態で呼び出すと、document.selection.createRange().textでドキュメントの選択部分の文字列が得られるので半角カナを。などのように数値参照形式に変換し、クリップボードにコピーできるものです。 geocitiesの掲示板は半角カナを全角に書き換えてしまうので作ったもの。 かなり適当なので自信ないですが。
ローカルにあるスクリプトなこともあり、このようにクリップボードを利用したり、ファイルを利用したりできます。 下の例はあんまり意味がないけどc:\windows\programs.txtを1行読みこんでは<BR>をつけて吐き出すもの。 これはVBScriptで記述。
このようにVBscriptのファイルアクセス機能やCreateObjectで作れる色々なオブジェクトを使用するとかなり色んなことができることになる。 次に、単にスクリプトだけでなく(irvineの様に)他の実行ファイルと連携する手法を見ていくことにする。 右クリックメニューでの起動の仕組み MenuExtにキーを追加するとき、ファイル名にEXEファイルを指定してもうまく実行してくれない。 HTMLファイルでなければ行けない理由はMSDNを見るとわかる。 About Browserの中のControll the ContextとかAdding to the Standard Context Menusを見ると、以下の様に記述されていることがわかる。 The URL will be loaded inside a hidden HTML dialog box, all the inline script will be executed, and the dialog box will be closed. The hidden HTML dialog's menuArguments property (on the external object) contains the window object of the window on which the context menu item was executed.要は、表示されはしないが、裏でHTMLがロードされてそのスクリプトが実行されることになる。 あくまで裏でHTMLの処理をしているだけなので、通常の実行ファイルなどは利用できない。 そのため、スクリプトを通してEXEファイルなどを実行しなければいけない。 スクリプトからの単純なEXEファイルの実行 一番単純な手法は、VBScript(別にJScriptでもいいが、以後VBScriptで見ていく)から利用できるWindows Scripting Host(WSH)の機能を利用することになる。 WSHは元々バッチファイルのようなものなので当然プログラム実行などの機能を持つ。 具体的には、WScript.Shellオブジェクトを作成して、メソッドRunを呼べばよい。 ファイル名の後に引数をつけることもできる。 例えば下の例ではメモ帳でc:\windows\programs.txtを開いている。
もし単にdocument.titleとかdocument.URLとかexternal.menuArguments.document.selection.createRange().text等の文字列を自作プログラムに渡したいだけならばこの手法がとりあえず手っ取り早い。 ただ、この手法ではプログラムには文字列とか、文字列化した数値ぐらいしか送ることができない。 また、そのために起動したプログラム側からdocumentやwindowオブジェクトを操作することができない。 スクリプトとの自作プログラムの連携 実際にdocumentやwindowオブジェクトを渡すためには、少なくとも引数をつけてRunするだけではだめなことがわかると思う。 ではirvineではどうしているかを見てみる。 irvineのインストールされたディレクトリを見ると、ie_menuというディレクトリがある。 右メニューでこの中のhtmlファイルのスクリプトが起動されることはレジストリを見るとわかると思う。 そこでは、 Set irvine = CreateObject( "Irvine.Api" ) とか、 irvine.Download Url,3 とか、 irvine.AddIRI "Url=" & Url & vbCRLF & "Comment=" & Comment & vbCRLF とかやっているのがわかる。 要は、Irvine.Apiというオブジェクトをレジストリに登録して、CreateObjectでオブジェクトして起動し、関数を呼ぶことでデータのやり取りをしていることがわかる。 この関数仕様はirvineにirvine.idlというタイプライブラリの形で公開されている。 つまり、自作プログラムと連携を取るならコのようにオブジェクトとしてレジストリに登録し、CreateObjectでオブジェクトを生成して関数呼出を行えばいいことになる。 VBではPro版以上であればActiveX EXEとかActiveX DLLが生成できるので、これを用いればよい。 例えば、CMTestというActiveX DLLのプロジェクトを作り、Testと言うクラスを作る。 一応生成や消滅のタイミングがわかるようにちょこちょこMsgBoxでメッセージを表示する。 このクラスはTestFuncという関数を持ち、オブジェクトを渡すとそのオブジェクトの持つdocumentオブジェクトからタイトルとURLを読みこみ、元のページに表示する。 右クリックメニューから呼ばれるのであれば、external.menuArgumentsを渡すことになる。
このプロジェクトをコンパイルすると、レジストリにCMTest.Test(プロジェクト名.クラス名)というオブジェクトがCMTest.DLLにあるという情報が登録される。(HKEY_CLASSES_ROOT\CMTest.Testができてるはず) もしこのプログラムを配布するのであれば、OCX等と同じように配布される側はインストーラを使うなりなんなりしてDLLを登録する必要がある。 次に、このオブジェクトをVBScriptから制御してみる。
流れを追ってみていくと、まずte1とte2の2つの変数にCMTest.Testオブジェクトを生成して代入する。 このとき、上のSub MainやClass_Initializeでメッセージボックスが表示される。 ちなみに順番は次の通り。
Class_Initializeはクラスのインスタンス生成時に呼び出されるものなので当然CreateObjectするたびに呼び出される。 次に、te1.TestFuncで実際にクラス内のTestFunc関数を実行している。 ここでは引数にexternal.menuArgumentsを渡している。 TestFuncでは引数O As Objectに対してO.documentを取得して変数docに代入し、titleプロパティやurlプロパティを取得したりwriteメソッドで元のHTMLが表示されていたページに文字列を表示したりしている。 引数で渡されたオブジェクトを操作できることがこれでわかる。 次に、変数te3にte1をSetしている。 これは参照をコピーしているだけで中身のコピーを行わないため、Class_Initializeは実行されない。 次にte1にNothingを代入しているが、上でte3とte1が同じオブジェクトを参照している状態にあるためまだte3がクラスへの参照を保持しているためクラスの破棄は行われない。 次にte3にもNothingを代入するとこのオブジェクトを参照するクラスはなくなるので破棄され、Class_Terminateが実行される。 te2に対しては特にNothingを代入するなどの処理は行っていないため、スクリプトを抜けるときに破棄される。 このような感じでVB製の自作プログラムの関数と連携ができることがわかった。 ここで注意なのが、一度このプログラムを実行すると、IEのウインドウを全て閉じるまでDLLが起動状態になり、ファイルの移動や変更ができなくなる。 しかし、再度このスクリプトを呼ばれるとまたSub Mainが呼ばれるようだ。 おまけ:バインディングのタイミング ここでは引数をObjectとして受け取ったが、なんでいきなりdocumentプロパティとかを操作できるか不思議かもしれない。 VB(やその他の言語)ではアーリーバインディング(事前バインディング)とレイトバインディング(実行時バインディング)を使うことができる。 この例では後者であり、実行時にオブジェクトの型からプロパティやメソッド名、引数の数などを取得し、チェックする。 実はexternal.menuArgumentsはHTMLWindow2と言う型のオブジェクトであり、external.menuArguments.documentで得られるのはIHTMLDocument2、external.menuArguments.windowはIHTMLWindow2(最初にIが付いているのがちょっと異なる)である。 これらの情報はObject型に対してTypeName関数を実行すると文字列でオブジェクトの型が取得できる。 レイトバインディングでは実行時にメソッド名などの対応を行うので、柔軟性はあるが、もしTestFunc関数の引数にexternal.menuArguments.documentをいれるとIHTMLDocument2はdocumentプロパティを持たないため、「そんなプロパティはありませんよ」とエラーがでて終了してしまう。 また、当然速度にも影響する。 もしこれを避けたいのであれば、アーリーバインディングを行えばよい。 引数をTestFunc(O As Object)とせずに、直接対象となるTestFunc(O As HTMLWindows2)というように定義すればよい。 HTMLWindows2にはdocumentプロパティが存在するので、コンパイルする段階で「そんなプロパティはありませんよ」というエラーが出ることはなくなる。 (代わりに「引数の型が一致しない」というエラーがでるが) コンパイルの段階でプロパティやメソッドのスペルや引数のミスが発見できる分安全性は高くなる。 レイトバインディングにも利点はあって、TypeName関数で得たオブジェクト名で分岐することで、関数のオーバーライド(同じ関数名で複数の複数の引数の型をとれるようにする)みたいなことができる利点がある。 HTMLWindows2とかIHTMLDocument2とかはVB標準のオブジェクトではないが、「プロジェクト」→「参照設定」で「Microsoft HTML OBject Library」をチェックしておくと利用できるようになる。 オブジェクトブラウザで見てみると、HTMLWindows2やIHTMLDocument2等が持つメソッドやプロパティをチェックすることができる。 おまけ2:Object型の実体 このObject型ってのは一体なんなのか。 MSDN等を見るとCOMにおけるIDispatchインターフェースであることがわかる。 このIDispatchはIUnknownから継承されているQueryInterface・AddRef・ReleaseのほかGetTypeInfoCount、GetTypeInfo、GetIDsOfNames、Invokeという関数を持つ。 AddRef、Releaseは参照されている数をインクリメント・デクリメントし、0になったら実際の解放処理を行うので、Set te1=Nothing等の時に利用されると思われる。 関数実行時にはGetIDsOfNamesで関数名の文字列に対応するID番号を取得し、GetTypeInfoで引数などを取得して、Invokeで関数を実際に呼び出す。 この仕組みを使ってレイトバインディングを実装していることになる。 詳しく知りたい人はMSDN等をIDispatchで調べてみるといいかもしれない。 |