気まぐれな戯れ言 バックナンバー8


気まぐれな戯れ言バックナンバーです。全体の一覧はこちら

04/02/29 CodecをつかったMP3のデコード・再生編−2
03/12/05 文字のアウトライン取得・操作編
03/11/25 IEのコンテキストメニュー拡張編

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

32ビットを以下の様に分割します。

AAAAAAAA AAABBCCD EEEEFFGH IIJJKLMM

それぞれの意味は以下の通り。
意味のところの色が濃くなっているのは重要な所です。

対応する文字ビット長意味詳細
A11同期用のビット11ビット全て1。これを探すことでフレームヘッダを探すことができる。
B2バージョン00-Mpeg 2.5、01-予約、10-Mpeg2、11-Mpeg1

広く使われているのはMpeg1やMpeg2である。そのため、サイトによっては「12ビットが同期用。次の1ビットが0-Mpeg2、1-Mpeg1」という表記がされている。
C2レイヤー数0-予約済み、1-LayerIII、2-LayerII、3-LayerI。

MP3はMpeg Audio LayerIIIといわれるだけあって通常1。
D1CRCコードの有無0-あり、1-なし。
E4ビットレート後述のテーブルの位置を示すことでビットレートを表す。逆を言えば固定ビットレートでは(16個のうち2個は別のために割り当てられているので)14種類しかビットレートの種類がない。128Kbpsの次がいきなり160Kbps、192Kbpsに飛ぶのもこれが原因。
F2サンプリング数後述のテーブルでサンプリング数を示す。大雑把に書くと、Mpeg1では32000Kbps以上、Mpeg2は16000-22050bps、Mpeg2.5では11025bps以下とここでMpegのバージョンの違いが大きく現れる。
G1パディングビット後でフレームサイズの微調整に利用。固定ビットレートでフレームごとのサイズが異なる主な原因。
H1未使用未使用
I2チャネルモードステレオ・モノラルの指定。ここでは関係ないので割愛。
J2拡張モード上のチャネルモードがジョイントステレオ指定の場合に利用。ここでは関係ないので割愛。
K1著作権1なら著作権保護あり。
L1オリジナル0-コピー、1-オリジナル。
M2強調エンファシス指定。ここでは関係ないので割愛。

ビットレートのテーブルはバージョン、レイヤーごとに異なり以下の用になります。
まぁ重要なのはLayerIIIだけでしょう。
(資料によってはバージョン2.5ではLayerIもLayerII、IIIと同じ値を取るというような記述が成されています。)
フリービットレートの0000は可変ビットレートで使用される値です。

ビットMpeg1、LayerIMpeg1、LayerIIMpeg1、LayerIIIMpeg2・2.5、LayerIMpeg2・2.5、LayerII・III
0000フリービットレート
0001323232328
00106448404816
00119656485624
010012864566432
010116080648040
011019296809648
01112241129611256
100025612811212864
100128816012814480
101032019216016096
1011352224192176112
1100384256224192128
1101416320256224144
1110448384320256160
1111未使用、エラー


次に、サンプリングレートのテーブルです。
前述の通り、バージョンの違いはここに大きく出てきますね。
ビットの順と実際の値の大小の順がずれているのが嫌ですが・・・
まぁMpeg1→Mpeg2→Mpeg2.5と半分ずつになっていくだけですね。

ビットMpeg1Mpeg2Mpeg2.5
00441002205011025
01480002400012000
1032000160008000
11予約済み

これで最初の4バイトからバージョン・レイヤー・ビットレート・サンプリングレートが取得できることになります。
ここから、以下の様にフレームサイズが計算できます。
Paddingは上の値の通り0か1です。

LayerIの場合:

(12 * BitRate / SampleRate + Padding) * 4

LayerII及びLayerIIIの場合:

144 * BitRate / SampleRate + Padding

また困ったことに、サイトによっては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フレーム当たりの秒数が下がるので、使えるバイト数も減りますね。

ビットレート32KHz44.1KHz48KHz
3214410496
40180130120
48216156144
56252182168
64288208192
80360261240
96432313288
112504365336
128576417384
160720522480
192864626576
2241008731672
2561152835768
32014401044860
(参考)
非圧縮・16bitステレオ
1152*4=4608

これを見て分かる通り、よく使われる44KHz・128Kbpsだとほぼ1/10に圧縮されることが分かります。

実際のフレームデータを見てみると、パディングがあるフレームとないフレームがばらついています。
それで全体として指定したビットレートにあうようにサイズ調整がされます。


これで、サンプリングレートやフレームサイズがわかったため、WAVEFORMATEXも埋めることが出来ますし、1つずつフレームをたどることが出来ます。
実際にByte配列のbuf(Index)から始まるフレームデータからWAVEFORMATEX(MPEGLAYER3WAVEFORMAT)を設定する関数は↓みたくなります。

Public Function GetMP3FormatFromBuffer(ByRef buf() As Byte, _
ByRef mw As MPEGLAYER3WAVEFORMAT, Optional ByVal Index As Long = 0) As Boolean
'バッファからMP3フォーマットの構造体を初期化する
Dim i&, j&, k&, Header(3) As Byte
Dim s$, FileID&
Dim MPVer&, Layer&, BitRate&, SampRate&, FrameLen&
Dim BitID&, SampID&, Padding&

If Index + 3 > UBound(buf) Then Exit Function

For i = 0 To 3
  Header(i) = buf(Index + i)
Next

'ヘッダチェック
If Header(0) <> 255 Or (Header(1) \ 32 <> 7) Then
  GetMP3FormatFromBuffer = False
  Exit Function
End If
  
  
'Mpegバージョンチェック
If (Header(1) And &H10) = 0 Then
  'Mpeg2.5
  MPVer = 3
Else
  If (Header(1) And &H8) = 0 Then
    'Mpeg2
    MPVer = 2
  Else
    'Mpeg1
    MPVer = 1
  End If
End If

'Layer
Select Case (Header(1) \ 2) And &H3
Case 3: Layer = 1
Case 2: Layer = 2
Case 1: Layer = 3
End Select
  
'bitrate
BitID = Header(2) \ 16
Select Case MPVer
Case 1
  Select Case Layer
  Case 1: BitRate = Choose(BitID + 1, 0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 0)
  Case 2: BitRate = Choose(BitID + 1, 0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 0)
  Case 3: BitRate = Choose(BitID + 1, 0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0)
  End Select
Case 2
  Select Case Layer
  Case 1: BitRate = Choose(BitID + 1, 0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 172, 192, 224, 256, 0)
  Case 2: BitRate = Choose(BitID + 1, 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0)
  Case 3: BitRate = Choose(BitID + 1, 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0)
  End Select
Case 3
  BitRate = Choose(BitID + 1, 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0)
End Select

'sampling rate
SampID = (Header(2) \ 4) Mod 4
Select Case MPVer
Case 1: SampRate = Choose(SampID + 1, 44100, 48000, 32000, 0)
Case 2: SampRate = Choose(SampID + 1, 22050, 24000, 16000, 0)
Case 3: SampRate = Choose(SampID + 1, 11025, 12000, 8000, 0)
End Select
  
Padding = (Header(2) \ 2) Mod 2
If Padding = 0 Then j = j + 1
FrameLen = 0
Select Case Layer
Case 1: FrameLen = (12& * 1000 * BitRate \ SampRate + Padding) * 4
Case 2, 3: FrameLen = 144& * 1000 * BitRate \ SampRate + Padding
End Select
  
'構造体を設定
With mw
  With .wfx
    .cbSize = MPEGLAYER3_WFX_EXTRA_BYTES
    .nSamplesPerSec = SampRate
    .wBitsPerSample = 0
    .nChannels = IIf(((Header(2) \ 4) Mod 4) = 3, 1, 2)
    .nAvgBytesPerSec = BitRate * 125
    .nBlockAlign = 1
    .wFormatTag = 85
  End With
  .nBlockSize = FrameLen
  .nCodecDelay = 700
  .nFramesPerBlock = 1
  .wID = MPEGLAYER3_ID_MPEG
  .fdwFlags = Padding
End With


GetMP3FormatFromBuffer = True

End Function


今回は短めですがここらへんで。(って↓のアウトラインの話が長すぎなんだな・・・)
次回に実際にデコードしてみます。

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構造体は以下のように記述される。
typedef struct tagBITMAPINFO { /* bmi */
   BITMAPINFOHEADER bmiHeader;
   RGBQUAD          bmiColors[1];
} BITMAPINFO;

typedef struct _TTPOLYCURVE { /* ttpc */
    WORD    wType;
    WORD    cpfx;
    POINTFX apfx[1];
} TTPOLYCURVE;
C言語でWindowsのプログラミングをやると上の様な表記に当たるが、それぞれ最後のbmiColors[1]やapfx[1]は文字通り配列が1個・・・というわけではない。
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の説明を修正)

result = GetGlyphOutline(hDC , uChar , uFormat , lpgm , cbBuffer , lpvBuffer , lpmat2)

hDC : 対象となるデバイスコンテキストであるため、フォントを指定したピクチャーボックスのhDCあたりを入れておけば良い。
uChar : 文字を1文字指定する。ここはAsc関数で文字を数値化して送ればよい。
uFormat : アウトラインを得るならGGO_NATIVE。アンチエイリアス処理を行ったビットマップを得るならGGO_GRAY2_BITMAPとかGGO_GRAY8_BITMAPを入れるが今回は関係ない。
lpgm : とりあえず関係ない。GLYPHMETRICS構造体の変数を入れておけば良い。
cpBuffer : 結果の入るバッファのサイズ。0の時は結果を格納するのに必要なバイト数を返す。
lpvBuffer : 結果の入るバッファ。NULLの時は結果を格納するのに必要なバイト数を返す。
lpmat2 : GGO_NATIVEの場合は関係ない。MAT2構造体の変数を入れておけば良い。

とりあえず、アウトライン情報が何バイトあるかわからないので、一端cpBufferのところをvbNullCharかByVal 0としてNULLを送り、返り値のサイズでバイト配列を確保して再度取得すればよい。
簡単に書くと下みたいな感じ。

Dim Buf() As Byte
Dim gm As GLYPHMETRICS
Dim m As MAT2
Dim bufsize&

'アウトラインを表すデータを取得
bufsize = GetGlyphOutline(hdc, Asc(t), GGO_NATIVE, gm, 0, vbNullChar, m)
If bufsize <= 0 Then Exit Function
ReDim Buf(bufsize - 1) As Byte
Call GetGlyphOutline(hdc, Asc(t), GGO_NATIVE, gm, bufsize, Buf(0), m)

ここで、バイト配列Bufの中にアウトラインの情報が入るので、次にこの情報を対応する構造体に格納することにする。
上記の通り、1つのブロックに複数のカーブ、1つのカーブに複数の制御点と、可変長の配列が二重になっているので少し面倒。

ちなみに、MFCマニュアルのCDC::GetGlyphOutlineには、以下のように記述されている。

描画文字のアウトラインとして、連続した輪郭線を返します。各輪郭線は、TTPOLYGONHEADER 構造体とそれに続く、文字描画に必要な情報を持ついくつかの TTPOLYCURVE 構造体で定義されます。すべての点は、POINTFX 構造体で返され、相対的な移動量ではなく、絶対位置で表現されています。TTPOLYGONHEADER 構造体の pfxStart メンバで指定される開始点は、アウトラインの輪郭線が始まる点です。それに続く、TTPOLYCURVE 構造体はポリラインまたはスプライン レコードです。ポリライン レコードは連続した点を表します。それぞれの点の間を直線で結ぶことにより、文字のアウトラインが描かれます。スプライン レコードは、TrueType で使われる 2 次曲線です (すなわち、2 次 b スプライン)。

正直、この文章だけで構造がわかる人は少ないと思う・・・
と思ったら、自分で解析後、 Microsoft Knowledge Base Article - 87115 HOWTO: GetGlyphOutline() Native Buffer Format をみつけたんだが・・・


まぁ上のも英語だしC言語だしわかりにくいので解析結果を書いて行きます。

データ構造は以下の通り。

取得したデータ(上記で言えばBufの中身)は複数のブロックからなる。
ブロック数は直接記述されていないため、事前に取得したサイズ(上記で言えばbufsize)まで読んで行く。

各ブロックは、下記のTTPOLYGONHEADER(サイズは16、8バイトの構造体であるPOINTFXについては後述)の後、各カーブのデータが続く。
やはりカーブの数は記述されていないため、ブロックサイズであるTTPOLYGONHEADERのcb分までカーブデータを読み込むことになる。
Type TTPOLYGONHEADER
        cb As Long           'ブロックサイズ
        dwType As Long       'アウトラインの種類。TT_POLYGON_TYPE
        pfxStart As POINTFX  'ブロックの始点
End Type

Type TTPOLYCURVE
        wType As Integer '曲線の種類(TT_PRIM_LINE(=1)-折れ線、TT_PRIM_SPLINE(=2)-ベジェ曲線)
        cpfx As Integer  'POINTFXの数
        apfx As POINTFX  '点の配列
End Type
上記のTTPOLYGONHEADERの後に各カーブを示すTTPOLYCURVEの配列が続くことになるが、各カーブの持つ制御点の数はバラバラであるため、1つ1つ構造体のサイズをチェックして行かなければならない。
このTTPOLYCURVEは曲線の種類を示すwTypeと、点の数を示すcpfxの後に、点の配列が続く。
最初の方で書いたとおり、VBではこの形の可変長配列を含む構造体を扱うことができないので、今回は以下のような構造体を作成した。

Type PointSingle
  x As Single
  y As Single
End Type

'1つのカーブ
Type FOCurve
  CType As Long
  numPoint As Long
  Point() As PointSingle
End Type
TTPOLYCURVEからwTypeとcpfxを読みこんでFOCurveのCTypeとnumPointに代入し、点の数で動的配列を確保して、1つずつ続く点を代入して行く。
(なぜアウトライン全体やブロック全体はバイト数で配列数を表すのにここだけ点の数なんだろう・・・。個々の点のデータ自体はPOINTFXで固定長だからかな?)

cpfxの分点を読みこみ終わったら次のTTPOLYCURVEを読んでいく。
途中でブロックサイズを示すTPOLYHEADERのcbバイトに達したらブロックが終了、次のブロックを読む。
全体のブロックサイズまで読んだら終了。

わかりにくいが、全体としては↓の感じ。
GetGlyphOutlineで得られるbufsizeバイト分のデータを自分でそれぞれ分解していくことになる。

アウトライン全体
(bufsizeバイト)
1つ目のブロック
(TTPOLYHEADER.cbバイト)
TTPOLYHEADER
(16バイト)
1つ目のカーブ
((4+8*cpfx)バイト)
TTPOLYCURVE
wType(2バイト)
cpfx(2バイト)
apfx[cpfx](8*cpfxバイト)
2つ目のカーブ
((4+8*cpfx)バイト)
TTPOLYCURVE
wType(2バイト)
cpfx(2バイト)
apfx[cpfx](8*cpfxバイト)
以後同様に
3つ目、4つ目・・・のカーブ
2つ目のブロック
(TTPOLYHEADER.cbバイト)
TTPOLYHEADER
1つ目のカーブ
2つ目のカーブ
以後同様に
3つ目、4つ目・・・のカーブ
以後同様に
3つ目、4つ目・・・のブロック

とこのような感じになっている。
可変長の部分の処理が面倒なので、サンプルでは、GetGlyphOutlineを呼んでアウトライン・ブロック・カーブの構造を表す下のような構造体を取得する関数を作成している。

Type PointSingle
  x As Single
  y As Single
End Type

'1つのカーブ
Type FOCurve
  CType As Long          '曲線のタイプ 1−折れ線、2−スプライン
  numPoint As Long       '制御点の数
  Point() As PointSingle '制御点の配列
End Type

'1つのブロック
Type FOBlock
  StartPoint As PointSingle  'ブロックの始点
  numCurve As Long           'カーブの数
  Curve() As FOCurve         'カーブの配列
End Type
  
'アウトライン
Type FontOutline
  Char As String             '対象の文字(1文字)
  NumBlock As Long           'ブロック数
  Block() As FOBlock         'ブロックの配列
  MaxPoints As Long          'もっとも多い制御点を持つカーブの制御点の数。(描画時に使用)
End Type

((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は基本的に小数もムリヤリ整数で扱いたいみたいだね。)

Type FIXED
        fract As Integer '小数部分
        Value As Integer '整数部分
End Type

Type POINTFX
        x As FIXED
        y As FIXED
End Type

まぁPOINTFXは2次元座標を表すのでxとyがあるのはいいとして、問題はFIXED構造体。
これは整数部分と小数部分をそれぞれIntegerで持っている。
整数部分はいいとして、小数部分は65536(=2^16)を分母にしたときの値を持っている。
要は普通の小数に直したい場合は Value + fract / (2^16) をとればよい。
ただ、問題としてVBのIntegerは符号付で-32768〜32767の値を持つため、以下のような関数FIXEDをSingle型に変換している。

Private Function FtoS(f As FIXED) As Single
 'FIXEDの固定小数点をSingleに
 Dim s As Single
 Dim i As Long

 s = f.Value
 i = f.fract
 If i < 0 Then i = &H10000 + i
 FtoS = s + CSng(i) / &H10000

End Function

これで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はメニューの表示される条件だが、

定数名表示条件
CONTEXT_MENU_DEFAULT0x1通常右クリックした時
CONTEXT_MENU_IMAGE0x2イメージを右クリックした時
CONTEXT_MENU_CONTROL0x4コントロールを右クリックした時
CONTEXT_MENU_TABLE0x8テーブルを右クリックした時
CONTEXT_MENU_TEXTSELECT0x10テキストを選択して右クリックした時
CONTEXT_MENU_ANCHOR0x20アンカー(リンク)を右クリックした時

のようになっている。
例えば通常の右クリック時とテキストを選択して右クリックした場合に表示したい場合は、0x1 OR 0x10 = 0x11となる。

レジストリを直接扱ってもいいし、もし配布したりすることを考えたら拡張子regのファイルを準備しておいて、
REGEDIT4

[HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\MenuExt\半角変換]
@="c:\\sample\\hankaku.html"
"Contexts"=dword:00000010

等と書いておくと、テキストを選択して右クリックを押すと「半角変換」というメニューが追加された状態になる。
クリックするとc:\sample\hankaku.html内のスクリプトを実行する。(パスの区切りが\\なのに注意)
ファイル名さえ書き換えてもらえるならhtmlファイルとregファイルを一緒にすれば、作ったものを配布できるでしょう。

このメニューの追加や削除のためのレジストリ操作を行ってくれるソフトにIE MenuExt(うりゅそふと)等のソフトが利用できます。

呼び出されるスクリプト

で、上述のファイル名の拡張子は.htmlになっています。
この理由として、このスクリプト起動の仕組みを見る必要がありますが、後述と言うことでとりあえずスクリプトを書いてみます。

html内なので、通常使えるのはJScript、VBScriptとなります。
メニューを呼び出す前のページの情報が欲しい場合は、external.menuArguments.windowやexternal.menuArgument.documentで取得することが出来ます。
これらのdocumentやwindowオブジェクトに対して行える操作はとほほのWWW入門などで確認できると思います。

例えば、簡単なサンプルとして

<script language="javascript">
  var doc=external.menuArguments.document;
  var url=doc.URL;
  var title=doc.title;

  doc.open("text/plain");
  doc.write("見てたページのURLは "+url+"\n");
  doc.write("見てたページのタイトルは "+title+"\n");
  doc.close;
</script>

とすると呼出元ページのURLとタイトルが表示されます。
(しかしこれ書いててHTMLソースを色づけして再度HTMLにするプログラム欲しくなってきた・・・)

もうちょい頑張ると色々できます。
下のものは、テキストを選択した状態で呼び出すと、document.selection.createRange().textでドキュメントの選択部分の文字列が得られるので半角カナを&#65377などのように数値参照形式に変換し、クリップボードにコピーできるものです。
geocitiesの掲示板は半角カナを全角に書き換えてしまうので作ったもの。
かなり適当なので自信ないですが。

<script language="javascript">

function KanaToNumber(iString){
  var reg1 = /%u([0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f])/;

  a=escape(iString);

  while(a.match(reg1)!=null){
    d=parseInt(RegExp.$1,16);
    if(d>=65377 && d<=65439) c="&#"+parseInt(RegExp.$1,16)+";";
    else c=unescape("%u"+RegExp.$1);
    b=a.replace(reg1,c);
    a=b;
  }

  a=unescape(a);
  return a;
}

var t=external.menuArguments.document.selection.createRange().text;

if(t!=""){
  t2=KanaToNumber(t);
  
  //改行、<、>、&、"の変換
  
  //t2=t2.replace(/\n/g,"<BR>\n");
  t2=t2.replace(/&/g,"&amp;");
  t2=t2.replace(/</g,"&lt;");
  t2=t2.replace(/>/g,"&gt;");
  t2=t2.replace(/"/g,"&quot;");
  
  clipboardData.setData("Text",t2);
}
</script>

ローカルにあるスクリプトなこともあり、このようにクリップボードを利用したり、ファイルを利用したりできます。
下の例はあんまり意味がないけどc:\windows\programs.txtを1行読みこんでは<BR>をつけて吐き出すもの。
これはVBScriptで記述。

<script language="VBscript">
  Dim fs,f,s,d,sh,te
  Set fs=CreateObject("Scripting.FileSystemObject")
  Set f=fs.OpenTextFile("c:\windows\programs.txt")
  Set doc=external.menuArguments.document

  doc.open()
  do until f.AtEndOfStream
    doc.write(f.ReadLine)
    doc.write("<BR>"+vbCrLf)
  loop
  doc.close()
  f.close()
</script>

このように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を開いている。

<script language="VBscript">
  Set sh=CreateObject("WScript.Shell")
  sh.Run("c:\windows\notepad.exe c:\windows\programs.txt")
</script>

もし単に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を渡すことになる。
(以下は標準モジュール)

Sub Main()
  MsgBox "Main"
End Sub

(以下はclsファイル) Sub TestFunc(O As Object) Dim url$, title$ Dim doc As Object Set doc = O.document title = doc.title url = doc.url Call doc.open("text/plain") Call doc.write("呼出元URL = " + url + vbCrLf) Call doc.write("呼出元タイトル = " + title) Call doc.Close End Sub
Private Sub Class_Initialize() MsgBox "Class_Initialize" End Sub
Private Sub Class_Terminate() MsgBox "Class_Terminate" End Sub

このプロジェクトをコンパイルすると、レジストリにCMTest.Test(プロジェクト名.クラス名)というオブジェクトがCMTest.DLLにあるという情報が登録される。(HKEY_CLASSES_ROOT\CMTest.Testができてるはず)
もしこのプログラムを配布するのであれば、OCX等と同じように配布される側はインストーラを使うなりなんなりしてDLLを登録する必要がある。

次に、このオブジェクトをVBScriptから制御してみる。
<script language="VBscript">
  Dim te1,te2,te3

  'オブジェクト生成
  alert("Create - 1")
  Set te1=CreateObject("CMTest.Test")
  alert("Create - 2")
  Set te2=CreateObject("CMTest.Test")

  'メソッド呼出
  alert("Call 1.TestFunc")
  te1.TestFunc(external.menuArguments)

  '変数をいじってみる
  alert("Copy 1 to 3")
  Set te3=te1

  alert("Delete 1")
  Set te1=Nothing
  alert("Delete 3")
  Set te3=Nothing
  alert("End")
</script>

流れを追ってみていくと、まずte1とte2の2つの変数にCMTest.Testオブジェクトを生成して代入する。
このとき、上のSub MainやClass_Initializeでメッセージボックスが表示される。
ちなみに順番は次の通り。
  • VBS内のalert("Create - 1")
  • Sub MainのMsgBox "Main"
  • Class_InitializeのMsgBox "Class_Initialize"
  • VBS内のalert("Create - 2")
  • Class_InitializeのMsgBox "Class_Initialize"
これを見てわかるように、MainはDLLロード時に1度だけ実行される。
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で調べてみるといいかもしれない。


一覧へ戻る