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


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

05/06/20 VBでOOP・STL風味編−2
05/05/19 VBでOOP・STL風味編−1
05/03/11 VBでOpenGL編−2

05/06/20 VBでOOP・STL風味編−2(異なるクラスに対するソート)[★★★★◎◎]
前回は汎用性のあるソートを行ってみましたが、今回はそれについてもう少し踏み込んでみます。

クラスの並べ替え 並べ替え対象のクラス作成
比較用クラス作成 実際にクラスを並べ替えてみる
まとめ
おまけ:型によらない代入


クラスの並べ替え

前回のプログラムでは、比較用クラスだけを入れ替えることによって整数配列に対して様々なソートを行いました。
この汎用ソートはコレクションで配列を渡すため、Variantで格納できる値なら何でもソート対象に出来ます。(もちろん比較用クラスが適切に作成されていれば、ですが)

そこで、今度は整数ではなくクラスを入れてみることにします。
1種類のクラスではつまらないので、StudentクラスとTeacherクラスが混在しているコレクションのソートをしてみることにします。

ここでは両クラスが共にHeight・Weight・Ageプロパティを持っているものとします。
後はHeightやWeightで比較するような比較用クラスを作成すればStudentだろうがTeacherだろうが関係なくソートできます。
VBのVariant型(やObject型)では、実行時に「そのメソッド・プロパティが存在するか?」をチェックするので、その比較用クラスが要求するプロパティさえ持っていればどんな型でもソートできることになります。

それとは別に、自分の属性を文字列で返すようなメソッドをStudentにもTeacherにも準備しておきます。
このメソッドも名前を揃えておけば中身に関係なく属性を文字列で返させることができます。
ここではGet_Stringメソッドとしています。

(本当はStudentとTeacherはPersonかなんかのベースとなるクラスを作ってImplementする方が望ましいです。
PersonにHeight・Weight・Age・Get_Stringを持たせておけば、StudentとTeacherは「〜〜のプロパティを持つか?」などと 実行時にチェックしなくても、実行前に型が確定する部分もあるので。
しかし、今回はどの道StudentやTeacherは一端Variantな変数やコレクションに代入されてしまい、「これはPersonをImplementしている」という情報は失われてしまうのでそこは省略しています。)



並べ替え対象のクラス作成

さて、実際にStudentクラスとTeacherクラスを作成してみます。
プロパティとしてHeight・Weight・Ageだけではつまらないので、StudentにはGrade(学年)、TeacherにはDomain(科目)というプロパティも持たせてみました。

(Studentクラス)
Public Height As Long , Weight As Long , Age As Long
Public Grade As Long

Function Create_Instance(ByVal m_Height As Long, ByVal m_Weight As Long, _ ByVal m_Age As Long, ByVal m_Grade As Long) As Student Dim ins As New Student ins.Height = m_Height ins.Weight = m_Weight ins.Age = m_Age ins.Grade = m_Grade Set Create_Instance = ins End Function
Function Get_String() As String Dim s$ s = "身長" + CStr(Height) + "cm , 体重" + CStr(Weight) + "kg , " + _ CStr(Age) + "才の" + CStr(Grade) + "年生" Get_String = s End Function
(Teacherクラス) Public Height As Long , Weight As Long , Age As Long Public Domain As String
Function Create_Instance(ByVal m_Height As Long, ByVal m_Weight As Long, _ ByVal m_Age As Long, ByVal m_Domain As String) As Teacher Dim ins As New Teacher ins.Height = m_Height ins.Weight = m_Weight ins.Age = m_Age ins.Domain = m_Domain Set Create_Instance = ins End Function
Function Get_String() As String Dim s$ s = "身長" + CStr(Height) + "cm , 体重" + CStr(Weight) + "kg , " + _ CStr(Age) + "才で担当科目は" + Domain + "の先生" Get_String = s End Function

どちらもCreate_Instanceというメソッドを作っていますが、これは初期値を与えつつ新しいインスタンスを作成するトリックみたいな手法です。
VBでは初期値を指定しつつクラスをNewすることが出来ません。
1つ1つNewした後で初期値を指定するのは面倒です。

なので、内部に初期値を指定するとその初期値を持つクラスを返してくれるような関数を作成しておきます。
とはいえ、Create_Instanceメソッドを呼ぶのにやはりインスタンスが必要なので、1つだけ静的にインスタンスを作成しておいて、後はそこからCreate_Instanceでポコポコとインスタンスを作成するという手法を取ります。

JavaやC++だとインスタンスを作らなくてもstatic属性のメソッドは呼ぶことが出来るし、そもそもNewの時に初期値も与えられるんでこんなことしなくてもいいんですけどね…


比較用クラス作成

では、比較用のクラスを作成してみます。
クラスのHeight・Weight・Ageのプロパティで比較するだけなのでそれほど難しくはありません。

(CmpHeightクラス)
Implements ICompare

Private Function ICompare_Compare(ByVal lhs As Variant, ByVal rhs As Variant) As Boolean ICompare_Compare = (lhs.Height < rhs.Height) End Function
(CmpWeightクラス) Implements ICompare
Private Function ICompare_Compare(ByVal lhs As Variant, ByVal rhs As Variant) As Boolean ICompare_Compare = (lhs.Weight < rhs.Weight) End Function
(CmpAgeクラス) Implements ICompare
Private Function ICompare_Compare(ByVal lhs As Variant, ByVal rhs As Variant) As Boolean ICompare_Compare = (lhs.Age < rhs.Age) End Function

いずれも前回同様ICompareをImplementsしています。
それぞれ異なるプロパティに対して大小比較の結果を返すだけの関数です。


実際にクラスを並べ替えてみる

実際に3つのボタンによってそれぞれ異なるソート結果をList1に出力するプログラムを書くと、以下の様になります。
Create_InstanceのためにStudent・Teacherを1つずつだけDim文で作成しています(stとte)

Private Sub Command1_Click(Index As Integer)
  Dim CmpClass(2) As ICompare
  Dim vec As New Collection
  Dim st As New Student, te As New Teacher
  Dim i&, v As Variant

  '各クラスはICompareをimplementsしているため代入可能
  Set CmpClass(0) = New CmpHeight
  Set CmpClass(1) = New CmpWeight
  Set CmpClass(2) = New CmpAge

  'VBではC++の様なstaticメンバ関数を作成できないため、
  'st、teと1つインスタンスを作っておいてそこから生成
  Call vec.Add(st.Create_Instance(160, 56, 18, 1))
  Call vec.Add(st.Create_Instance(175, 60, 19, 1))
  Call vec.Add(st.Create_Instance(165, 50, 19, 2))
  Call vec.Add(st.Create_Instance(155, 55, 20, 2))
  Call vec.Add(st.Create_Instance(168, 70, 20, 3))
  Call vec.Add(st.Create_Instance(180, 60, 23, 3))
  Call vec.Add(st.Create_Instance(162, 49, 28, 4))
  Call vec.Add(te.Create_Instance(170, 62, 31, "国語"))
  Call vec.Add(te.Create_Instance(164, 62, 40, "数学"))
  Call vec.Add(te.Create_Instance(170, 59, 27, "数学"))
  Call vec.Add(te.Create_Instance(164, 54, 35, "数学"))
  Call vec.Add(te.Create_Instance(181, 70, 50, "英語"))

  'ボタンのIndexによって渡されるICompareが変わる
  Call Algorithm_Sort(vec, CmpClass(Index))

  Call List1.Clear

  '中身はstudentにしてもteacherにしてもGet_Stringを実装していればよい
  For Each v In vec
    List1.AddItem v.Get_String
  Next

End Sub

例えばIndex=0のボタンを押した場合、CmpHeightを使ってソートするので出力結果は以下の様になります。
文字列化のためのメソッドも名前を統一してあるので、StudentもTeacherも関係なく出力できます。

身長155cm , 体重55kg , 20才の2年生
身長160cm , 体重56kg , 18才の1年生
身長162cm , 体重49kg , 28才の4年生
身長164cm , 体重62kg , 40才で担当科目は数学の先生
身長164cm , 体重54kg , 35才で担当科目は数学の先生
身長165cm , 体重50kg , 19才の2年生
身長168cm , 体重70kg , 20才の3年生
身長170cm , 体重62kg , 31才で担当科目は国語の先生
身長170cm , 体重59kg , 27才で担当科目は数学の先生
身長175cm , 体重60kg , 19才の1年生
身長180cm , 体重60kg , 23才の3年生
身長181cm , 体重70kg , 50才で担当科目は英語の先生


まとめ

前回作成したソートプログラムを利用すると、クラスに関してもソートを行うことが出来ました。
また、VBの「動的にメソッド・プロパティの存在を確認する」という特性を用いて、異なるクラスの混ざった状態でもソートをうまく行うことが出来ました。
Student・Teacher以外にもHeightとWeightとAgeとGet_Stringさえ持っていればどんなクラスでも同じようにソートすることが出来ます。
まぁ今回も速度的にはかなり遅そうですが、整数のソートに比べると複雑な機能がわりとさらっと実現できるのは面白いところだと思うのですがどうでしょうか。

ソートはここまでにして、次回は配列の要素それぞれに対して関数を呼び出すということをしています。
STLで言うところのfor_eachに相当します。
さらに、ちょこっとfunctorみたいな事をしてみる予定。(Binder1stとか…)
単にfor_eachやfunctorだけではSTLの劣化コピーでしかないので、VBならではの手法として規定プロパティを利用してさらにfor_eachをトリッキーに利用してみようかと思います。

まぁプログラム実験部屋に先にコード出してますが(^^;

おまけ:型によらない代入

今回の様なプログラムを扱う場合、Variant型の中に整数や小数の様な型が入ることもあれば、クラスが入ることもあります。
ここで厄介となるのが、「クラスの代入にはSet文が必要である」ということです。
あるVariant型変数vの中身がクラスかどうかわからないけど別の変数aに代入したい場合、以下の様に書く必要があります。

'中身がオブジェクトかどうか関わらず代入
If IsObject(v) Then
  Set a = v
Else
  a = v
End If

まぁ面倒ですが一応解決できています。
では、aに代入するのがvではなくて何か計算式だとか関数の返り値だったらどうするか。
例えば関数SomeFuncというものがあったとします。
そのままvのあった場所に当てはめると、まずIsObject関数でオブジェクトかどうかを判定するためにIsObject(SomeFunc())とすると…

If IsObject(SomeFunc()) Then
  Set a = SomeFunc()
Else
  a = SomeFunc()
End If

またSet a = SomeFunc()とかa = SomeFunc()の様に2回関数を実行する羽目になります。
これは処理時間的にも問題ですし、そもそもStatic変数やグローバル変数が絡んでたりすると2回の結果が同じとは限りません。

「じゃあ一端SomeFunc()の結果を変数に代入すればいいじゃん」
って今やろうとしていること自体が代入なわけで、このままではぐるぐるめぐり。

「SomeFunc関数の中で別の変数に結果を保存しておいて、別にその結果を再度取り出す関数を作れば?」
確かにSomeFunc関数自身が返り値をクラスかそうでないのか知っているならばうまく行きそうですが、今回のソートの様な場合関数内で扱う値が結局クラスかどうかわからないので、「別の変数に結果を保存」で結局代入の問題がでてきます。


結局どこかでこの問題を解決する手法を考えないといけません。
解決法としては、「クラスだろうとそうでなかろうととにかく一端変数に代入する」ということがあります。
まぁそれが出来ないから苦労してるんじゃないかと言われればそれまでですが、一応解決策は存在します。
とりあえず思いついたのが2つなので両方紹介しておきます。
速度的にはどっちも微妙ですが…

  • 一端コレクションにAddしてしまう

    変数に代入するのを諦めて、コレクションに入れてしまおうというもの。
    以下の様に一時的にhogeというコレクションを作成しておけば、あとはhoge.Item(1)でその値が参照できます。

    Dim hoge As New Collection
    Call hoge.Add(なんか値とか関数とか)
    
    あとはhoge.Item(1)で利用
    

    毎回一時的な変数を準備するのは面倒ですが、速度的にはそれほど悪くないのではないかと。

  • 代入用の関数を作ってその引数にしてしまう

    値がクラスであれ整数であれ、関数呼び出しの引数にしてしまうと、関数の中からはその値はある変数に格納されて見えます。
    関数呼び出しの時に「クラスだからSetをつけて〜」なんてやる必要はないです。

    Sub Substitute_Variant(var As Variant, v As Variant)
    
    '中身がオブジェクトかどうか関わらず代入
    If IsObject(v) Then
      Set var = v
    Else
      var = v
    End If
    
    End Sub
    
    あとはCall Substitute_Variant(a , SomeFunc())でさっきの問題も解決
    

    コレクションを使う手法は余計な変数が出てきたりするのでソースだけからはわかりにくいですが、こちらの手法は「ああ変数に代入しているんだな」ということはわかりやすいです。
    ただ、毎回関数呼び出しをするとちょっとコストは高いかも知れないです。
    こっちの手法は「Setを省くため」というよりは、「毎回IsObjectするのは面倒だから関数にしてしまえ」というアプローチで先に思いつきやすい手法だとは思います。
これらの手法を使うと、何とか代入の度にIsObjectと書く必要はなくなります。

しかし、正直Setと書かされる言語仕様はどうかと思いますね…
クラスでない変数の代入ももともとLetが必要だったのが省略されたんで、こっちも省略可能にしてしまえばいいのに。
VBにとっては、Variant型の代入の時にはどのみち両辺の型をチェックしてから代入するんだし、Variantでないときはコンパイル時にクラスかどうかわかるんだし、わざわざSetと書かせるメリットというと、利用者に「これはクラスの代入なんだ」と意識させることぐらいしか思いつかないです。

この代入の話はソートだとどうってことはないですが、for_eachみたいな事をやろうとすると気になるところなので今回のうちに扱っておきました。
(というか自分がやってて気になった…)

05/05/19 VBでOOP・STL風味編−1(汎用ソートアルゴリズム)[★★★★◎]
VBはオブジェクト指向の言語としては非常に未熟とか機能不足とか言われます。
実際そうなわけですが、VBの持っている機能でもがんばればそこそこまで行ける!ということで、無理やりな利用法にチャレンジ。

今回のはC++やJava等、オブジェクト指向寄りな言語を使ったことが無いとちょっとわかりにくいかもしれないです。
さしあたりVBのクラスモジュールの機能を知っていると読みやすいと思います。
C++のSTLをベースにするのでSTLを知っているとさらによし。

今回はSTLにあるsort関数っぽいものをVBで再現したいと思います。
今回の内容はプログラム実験部屋にプログラムを置いてあります。

汎用性のあるソートアルゴリズム  VBにおいて呼び出す関数を動的に変えるには?
比較方法を表すクラスを考えてみる  ソートの中身を考える
整数を並べ替えてみよう  まとめ  補足:ICompareの意義?


汎用性のあるソートアルゴリズム

プログラムを書いていると、配列などのデータ群をソートする場面はよくあります。
ただ、VBでこれらのプログラムを書く場合、型ごとやソートの順番ごとにソートの関数を作成したりすることは多いと思います。
しかし、大抵の場合ソートは、

「なんらの基準で2値を比べて、並べ替える」

という処理になっています。
極端な話、数値のソートと文字列のソートなんて大小比較の部分がちょっと違うだけで後は同じ処理でソートできます。
(まぁ場合によってクイックソートが使いたいとかヒープソートが使いたいとかあるかも知れませんが・・・)

そこで、共通化できる部分は共通化し、変えたい部分(上の例では大小比較の部分)だけ変えることで使いまわしの出来るソート関数を考えて見たいと思います。

実際C++のSTLのソートではそのような設計になっています。


VBにおいて呼び出す関数を動的に変えるには?

さて、上の様に大小比較の所だけ変えるようにしたい場合、比較関数を用意しておいて、必要に応じて呼ぶ関数を変えることで大小比較のやり方も変えることが出来ると便利です。
必要に応じて呼ぶ関数を変えることは出来るのでしょうか。

C言語などには関数ポインタという概念があります。
関数ポインタの中身を書き換えることで動的に呼ぶ関数を変えることが出来ます。

VBでは関数のアドレスはAddressOf演算子で取得することが出来ますが、そのアドレスの関数を呼ぶことは出来ません。
(コールバックを用いるAPIを使ってトリッキーにやることは出来るかもしれませんが、言語仕様としては無理)

では、どのように関数を動的に変えるか。
ここで、VBだとそれほど有効に利用されていない(気がする)クラスモジュールが登場します。

VBで、あるクラスA型の変数aとB型の変数bがあったとします。
どちらもあるメソッドhogeだけが利用できるとします。
a.hogeだとAのhogeメソッドが呼ばれ、b.hogeだとBのhogeメソッドは呼ばれるのは当たり前のことです。

では、次の様により汎用の型に代入して実行してみるとどうなるでしょうか。

Dim a As New A
Dim b As New B
Dim c As Object

Set c = a
Call c.hoge
Set c = b
Call c.hoge

ちゃんとそれぞれのhogeメソッドが呼ばれると思います。
これでわかることは、呼ぶ関数を直接切り替えることは出来なくても、同じメソッドを持つクラスならより汎用的なクラス(ここではObject型)に代入することで呼ぶメソッドを変える事が出来ると言うことです。

この例では、クラスAもBは(まぁ他の機能もあるのかもしれませんが)さしあたり呼ぶ関数を切り替えるためだけのクラスといえます。
このように、関数のように振舞うオブジェクトのことを関数オブジェクトと言います。

これを使うと、ソートの大小比較をクラスで表して、そのインスタンスを切り替えることで比較のやり方を動的に変えられるような気がしませんか?


比較方法を表すクラスを考えてみる

ここまでで、どうやらクラスをうまく使えば比較方法を動的に切り替えることが出来そうだということがわかったと思います。
ただ、上の例でもわかるように異なるクラスでもメソッド名が一致すればうまく呼び出せますが、メソッド名がわからないとどうにもなりません。
ある比較用クラスではLessというメソッドかもしれないし、他ではGreaterとかCompareとかBiggerとか名前がばらけてしまう可能性があります。

そこで、ある程度比較用クラスの仕様は統一しておく必要があります。
そのためにVBではImplementsというステートメントを利用できます。
JavaのImplementsとほぼ同じ意味ですね。

比較用のベースとなるクラスを作っておき、各比較関数はベースのクラスに対してImplementsすることでメソッド名などの仕様をあわせることが出来ます。

ここで、比較用のベースとなるクラスICompareを作成してみます。
比較用のクラスは、必ずCompareメソッドというメソッドを持つことにしてみましょう。
ICompareは以下の様な感じになります。

(ICompareクラス)

Public Function Compare(ByVal lhs As Variant, ByVal rhs As Variant) As Boolean
End Function

メソッドの中身は空で構いません。
中身はこれをImplementsするクラスが生めていきます。
というよりも比較用クラスは、このCompareメソッドを作る必要があります。
さしあたりこのメソッドは2つのVariant値を取ってBooleanを返すメソッドだということを示したことになります。


ソートの中身を考える

さて、ようやくImplementsするクラスを作るか…と行きたい所ですが、先にソート全体の仕組みを考えておきます。
ソートするからには、ソートする対象となる配列か何かが必要です。
ただ、VBの場合配列というと決まった型の配列になってしまいます。
そこで、今回はCollectionを使用します。
ユーザー定義型が 代入できないのは少し痛いですが、Variant型に代入できる型であれば、整数でも小数でも文字列でも、あとはクラスでもほうりこめます。

Collectionと比較用クラスを引数に取って、Collectionの並べ替えを行うソート関数は以下の様に書くことが出来ます。
(2005/06/20修正)

Sub Algorithm_Sort(dat As Collection, CompClass As ICompare)
Dim i&, j&
Dim tv As Variant, tv2 As Variant

'まぁ今回はバブルソート
For i = 1 To dat.Count - 1
  For j = dat.Count To i + 1 Step -1
    '大小関係が反転すべき
    If CompClass.Compare(dat(j), dat(i)) Then
      'コレクションは直接中身を書き換えられないのでadd+removeで
      
      '入れ替え
      If i + 1 = j Then
        '1つ後ろなだけならiを後ろに回す
        Call dat.Add(dat(j), , i)
        Call dat.Remove(i + 2)
      Else
        'そうでない場合
        Call dat.Add(dat(j), , i)
        Call dat.Add(dat(i + 1), , j + 1)
        Call dat.Remove(i + 1)
        Call dat.Remove(j + 1)
      End If
    End If
  Next
Next

End Sub

ソートの手法は別にクイックソートでも何でもいいんですが、ここではわかりやすくバブルソートにしてみました。
このアルゴリズムでは、単純に隣同士の2値を比較して並び順が逆の方がよい(Compareの結果で判断)のであれば入れ替えるということを延々と繰り返します。
Collectionでは直接値を交換することは出来ないようなので、一端Collectionからはずして挿入しなおすということをしています。
(速度的には非常に無駄が多いですが、今回の本題は速度では無いのでそこは一端おいといてください。)

Collectionの中身がオブジェクトでもそうでないものでもいいように、IsObjectで中身がオブジェクトかどうかチェックしつつ入れ替えを行っています。


整数を並べ替えてみよう

上のソート関数を使って実際に整数を並べ替えて見ます。
入力の引数はCollectionと比較関数です。
Collectionはソート対象の整数をどんどん入れていくだけでOKです。

となると、ソートを行うためには比較関数を作る必要があります。
まぁどんな並べ替えでもいいんですが、ここでは単純に
「小さい順」「大きい順」「文字列化して小さい順」
の3種類のクラスを作ってみます。

各クラスはICompareクラスをImplementsし、Compareメソッドの中身を埋めればOKです。
Compareメソッドは2値を比較してBooleanを返すだけなのでそれほど深く考えず作って見ましょう。

(LongLessクラス)

Implements ICompare

Private Function ICompare_Compare(ByVal lhs As Variant, ByVal rhs As Variant) As Boolean

  '整数値で比較して左側の値が小さければTrue
  ICompare_Compare = (CLng(lhs) < CLng(rhs))

End Function

(LongGTクラス) Implements ICompare Private Function ICompare_Compare(ByVal lhs As Variant, ByVal rhs As Variant) As Boolean '整数値で比較して左側の値が大きければTrue ICompare_Compare = (CLng(lhs) > CLng(rhs)) End Function
(StrLessクラス) Implements ICompare Private Function ICompare_Compare(ByVal lhs As Variant, ByVal rhs As Variant) As Boolean '文字列で比較して左側の値が小さければTrue ICompare_Compare = (CStr(lhs) < CStr(rhs)) End Function


ここまでやると、ようやく準備終了です。
実際にこのソート関数と比較クラスを使ってソートをして見ます。
ここではコマンドボタンCommand1を3つ作り、それぞれIndexが0〜2となるようにしていたとします。

Private Sub Command1_Click(Index As Integer)
  Dim CmpClass(2) As ICompare
  Dim vec As New Collection

  '各クラスはICompareをimplementsしたもの
  Set CmpClass(0) = New LongLess
  Set CmpClass(1) = New LongGT
  Set CmpClass(2) = New StrLess

  '適当な値を作成
  Call vec.Add(1851)
  Call vec.Add(9353)
  Call vec.Add(2748)
  Call vec.Add(926)
  Call vec.Add(1166)
  Call vec.Add(12703)
  Call vec.Add(40)
  Call vec.Add(66916)
  Call vec.Add(5562)

  '呼び出す関数はIndexの値でクラスが切り替わる
  Call Algorithm_Sort(vec, CmpClass(Index))

(なんかしらの形で結果を表示。サンプルではリストボックスを使用)

End Sub

さて、結果はどうなるかというと・・・
Index012
使ったクラスLongLessLongGTStrLess
結果 40
926
1166
1851
2748
5562
9353
12703
66916
66916
12703
9353
5562
2748
1851
1166
926
40
1166
12703
1851
2748
40
5562
66916
926
9353

結果を見ると、確かに意図したとおりの結果になりました。


まとめ

今回は1つの汎用ソート関数Algorithm_Sortを作成し、後は比較用のクラスを作っただけで1種類の関数でソートの方法を変えることが出来ました。
C++のSTLあたりだとガリガリ速度面で最適化してくれるんですが、VBだと残念ながら余り最適化は望めません。
Algorithm_Sortにどんな比較クラスが入ってくるかわからないし、CollectionはCollectionで中身が整数か文字列かクラスかわからないからです。

後、残念なのがユーザー定義型の並べ替えが出来ないことでしょうか。
まぁ入力をCollectionでは無く、ユーザー定義型の配列にすれば出来るんですが、そうするとその関数は特定のユーザー定義型専用になってしまいます。
それでも構わない(どうせ1種類の型でしか使わないなど)場合はいいかもしれません。

というわけで速度的には余り有効ではありませんが、スマートにプログラムをすることを考えるとなかなか面白くないですかね。
次回はこのAlgorithm_Sortを使ってもうちょい色々やる予定です。


補足:ICompareの意義?

今回比較用クラスはICompareクラスをImplementsして作成しました。
しかし、このICompareは本当に必要でしょうか?

最初のクラスA・BをObject型に代入している例を見てみると、別にA・Bはメソッドhogeを持つクラスをImplementsしているわけではありません。
それでもなぜちゃんとメソッドhogeが呼べるかというと、メソッドの呼び出しの時に「そのクラスはhogeというメソッドを持つかどうか?」を実行時に判断しているからです(レイトバインディング)。

じゃあ比較クラスもICompareなんて使わずに、Objectを使えばいいじゃんと思うかも知れません。
実際Algorithm_Sortの引数CompClassの型はICompareでなくObjectでも問題なく動きます。

ただ、ICompare型を間に挟む利点として以下の要素が考えられます。

  1. まずは実行前にCompareメソッドが存在するということがVBやコンパイラにわかるということがあります。

    Algorithm_Sortの中身を書いているとき、CompClassがObject型だとすると、「CompClass.Compare」と入力するときにメソッド名の補完機能が働きません。
    これは当たり前で、CompClassがどんなメソッドを持っているかなんてわからないからです。
    一方、CompClassがICompare型だとすると、なんかわからないけど2つの引数を持つCompareというメソッドが存在していることがVBにわかります。
    そのため、補完機能でメソッド名を入力することが出来ます。

  2. 次に、↑の理由で型チェックの機構が働くことです。
    Object型だとどんなクラスも代入できてしまうので、もしAlgorithm_Sort関数の引数にCompareメソッドを持たないクラスが渡されてしまってもその段階で発見することは出来ません。
    実際にCompareメソッドを実行する段階で「そんなメソッドありません」とエラーが発生します。

    一方、引数がICompare型だとわかっていれば関数呼び出しの段階で発見できます。
    (コンパイラがコンパイルの段階で判断できるかな…と思ったらダメだったみたいだ。C++ならそのぐらいまでチェックするんだけど…)

  3. まぁ最大の利点は、VBだけでなく何よりも利用者自身が「これはCompareメソッドを準備しなきゃいけないんだな」とわかるということがあります。
    クラスを作成する際に比較用のメソッドの仕様が決まっているので、それにしたがってメソッドを作るだけで比較用クラスを作ることが出来ます。
逆に弱点としてはVBだと1クラス1ファイルなので、小さいクラスのためにクラスファイルがたくさん出来てしまうことですかねぇ…

05/03/11 VBでOpenGL編−2(ライティング・テクスチャ)[★★◎◎]
(自分で書いててナンですが、今回はあまり乗り気でないこともあって出来がかなり微妙。)

前回はOpenGLの初期化・終了についてやりました。
今回はライティング・テクスチャと通常の3Dプログラムを作る際の基本的なものについて扱います。

前回も書きましたが、ここでの内容は「VBからOpenGLを使うこと」を重視しており、OpenGLそのものの活用法についてはC言語とそう変わりはないのであまり細かく触れません。
C言語からOpenGLを使う場合は、OpenGLプログラミングコースがオススメです。
簡単な3Dの概念の説明もあるので、行列変換などの話はそちらを参考に。

で、3D・ライティング・テクスチャについて「VBから使う点で必要なこと」を重視してみていきます。
3Dの基本  行列による変形  ライティング  テクスチャ  最後に
3Dの基本

前回は2次元の3角形を描画しましたが、今度は3Dの描画をして見ます。
なお、ここの内容はVBならではの内容は特になく、C言語から利用する際も同じなので、OpenGLプログラミングコースを見るのもいいでしょう。

2次元の時はglBegin〜glEndの間にglVertex2fを頂点の数だけ記述しました。
glVertex2fは2とついているように2つの引数、つまりXとYだけ指定しました。
3次元にする場合は単純にglVertex3fを使ってX,Y,Zの3引数を送ればいいです。


現在のままでは、描画した順番に図形が描画されますが、実際は奥にある面は手前にある面で隠されなければいけません。
そこで、描画の際Zバッファを利用するように指定します。
これはglEnableでZバッファとの値の比較(glcDepthTest、C言語ならGL_DEPTH_TEST)を有効にするだけでOKです(下のプログラムの上2行)。
各フレームの最初にglClearでバッファのクリアを行う場合は、clrDepthBufferBlt(C言語ならGL_DEPTH_BUFFER_BIT)も指定してZバッファもクリアする必要があります(下2行)。

'Zバッファによる奥行きチェックを行う
Call glEnable(glcDepthTest)
  
'バッファのクリア
Call glClear(clrColorBufferBit Or clrDepthBufferBit)

後は3次元で描画するので適切な透視変換などの行列を設定することになります。
まず、カメラのレンズに相当する射影の処理に関しては、OpenGLでは透視投影を行う場合はglFrustumまたはgluPerspective、平行投影を行う場合はglOrthoまたはgluOrtho2Dの各関数を使ってパラメータを設定します。
ここら辺は単に引数に値を設定するだけなので、上のOpenGLプログラミングコースの通りに値を設定すればいいでしょう。


行列による変形


ある物体を描画するとき、物体全体を拡大させたり回転させたりしたい場合があります。
しかし、各頂点でプログラム側で回転させたりするのは面倒です。
OpenGLでは、変形に対応する行列を設定しておくと各頂点に変形を加えてくれます。

よくある変形として、平行移動や回転がありますがこれは既にOpenGLに準備されています。
単純な回転はglRotatef、拡大縮小はglScalef、平行移動はglTranslatefで指定することが出来ます。
これらは引数に回転角度や拡大率を指定すればいいのですが、問題は任意の行列を指定したい場合です。

OpenGLに行列を渡したい場合にどのようにするかということが問題になります。
OpenGLでは行列は基本的に4x4となります。
行列を指定する命令はglLoadMatrixf、glMultMatrixfがあります。
前者は指定した行列を代入し、後者は現在の行列に指定した行列を掛け合わせます。

じゃあ、行列ってどうやって渡すのよ、という話。
ここら辺でようやく「VBであるために気にすべきこと」が登場ですね。

4x4の行列を表現するとなると、普通に考え付くのが4x4の要素を持つ2次元配列か、16個の要素を持つ1次元配列を使うことになります。
実際には、どちらでも利用することが出来ます。
まず、16個の要素を持った配列a(15)を考えた場合、4x4の行列の各要素は以下の様に対応します。

a(0)a(4)a(8)a(12)
a(1)a(5)a(9)a(13)
a(2)a(6)a(10)a(14)
a(3)a(7)a(11)a(15)


直感的には、横方向に配列のインデックスが進んでいきそうな感じですが、実際には縦に進みますので注意です。

次に、4x4の2次元配列を考えてみる前にVBでの多次元配列の要素がメモリ中にどのように並んでいるかを考えて見ます。
例えば3次元の配列a(2,2,2)があったとすると、VBでは
a(0,0,0)、a(1,0,0)、a(2,0,0)、a(0,1,0)、・・・、a(2,2,0)、a(0,0,1)、・・・、a(2,2,1)、a(0,0,2)、・・・、a(2,2,2)
と前にある要素の値が1つずれると隣の要素になります。
後ろの要素ほど1値がずれると大きく配置が変わります。
(C言語ではa[0][0][0]、a[0][0][1]、a[0][0][2]と順序が逆)

ここでa(3,3)というような2次元配列を考えると、実際にメモリには
a(0,0)、a(1,0)、a(2,0)、a(3,0)、a(0,1)、・・・、a(3,3)
の順に並んでいます。
これをa(15)の1次元配列の時の様に行列に当てはめると、
a(0,0)a(0,1)a(0,2)a(0,3)
a(1,0)a(1,1)a(1,2)a(1,3)
a(2,0)a(2,1)a(2,2)a(2,3)
a(3,0)a(3,1)a(3,2)a(3,3)

となります。右に行くと2つ目の要素が大きくなり、下に行くと1つ目の要素が大きくなります。
これは数学で行列Aの第i行j列目の要素を表すAijという表記と近くて扱いやすいですね。
さらに配列を定義するときに
Dim a(1 to 4,1 to 4)
の様にインデックスを1スタートにしておくと数学上の表記と一致し、さらに扱いやすくなります。
(C言語では列と行が逆になってしまい、ちょっと直感的でない…)


ここで説明したように、16個の要素を持つ1次元配列か4x4の2次元配列を用いて4x4の行列を表現することが出来ます。
値をセットした後、OpenGLに渡すには次の様にします。(それぞれ1次元・2次元の場合)

Call glLoadMatrixf(a(0))
Call glLoadMatrixf(a(0,0))

なんで行列を渡すのに最初の要素だけでいいのか?と思うかもしれませんが、通常glLoadMatrixの引数はByRefで定義されています。
ByRefでは実際には指定した値のアドレスが渡されますので、OpenGL側で他の要素も順に取得できることになります。
これは次のライティングでも重要です。


以下の例は実際にVBで行列を指定する例です。
以下の2つの図は、左側は普通に立方体を描画したものであり、右側はZ軸で45度回転した上にY軸を中心に45度回転したものです。

 

Z軸およびY軸の回転はそれぞれに行列を作り、glMultMatrixを用いて掛け合わせています。
具体的には以下の様なコードになります。

Dim a(3, 3) As Single, b(3, 3) As Single
'Z軸を中心に45度回転し、さらにY軸を中心に45度回転
'実際の処理とは逆の順番で掛ける

a(0, 0) = 1 / Sqr(2)
a(0, 2) = -1 / Sqr(2)
a(1, 1) = 1
a(2, 0) = 1 / Sqr(2)
a(2, 2) = 1 / Sqr(2)
a(3, 3) = 1

Call glMultMatrixf(a(0, 0))


b(0, 0) = 1 / Sqr(2)
b(0, 1) = 1 / Sqr(2)
b(1, 0) = -1 / Sqr(2)
b(1, 1) = 1 / Sqr(2)
b(2, 2) = 1
b(3, 3) = 1
Call glMultMatrixf(b(0, 0))


ライティング

基本的にはglEnable(glcLighting)やglEnable(glcLight0)でライティングをオンにし、glVertexで頂点を指定する際に同時にglNormal3f等で法線を割り当てていくだけです。
そこら辺は上にもあるOpenGLのプログラミングコースを見てください。

上の行列関連の命令では、行列をOpenGLに渡す必要がありましたが、このときはByRefを用いて配列の先頭のアドレスを渡すということがありました。
ライティングでも色の値やベクトルを配列を用いて渡す機会があります。

OpenGLのライティングでは、光源の位置や光の色を指定するのにglLightfvという関数を使います。(fはFloat型(VBで言うSingle)、vはベクトルを表す。型が違うglLightdvなんかもある)
ここでは光源の座標や光の色のRGBAの値を要素数4の配列を用いて引数に渡します。

例えば、拡散反射の色値をスクロールバーから読み取って指定するには以下の様にします。
glLightfvに渡すのは配列の先頭の要素だけというのは上の行列の話と同じですね。

'拡散反射
col(0) = HScroll1(0).Value / 255
col(1) = HScroll1(1).Value / 255
col(2) = HScroll1(2).Value / 255
col(3) = 1
Call glLightfv(ltLight0, lpmDiffuse, col(0))


ライティングに関してVBならではの部分はこのぐらいなので、あとはプログラミングコースを見ながらやれば色々出来るでしょう。


テクスチャ

OpenGLではメモリの内容を読み込んでテクスチャとして利用することが出来ます。
メモリの内容をテクスチャとして読み込むにはglTexImage2Dという関数で行うことが出来ます。
読み込んだテクスチャの使い方の概論はプログラミングコースを参照してください。
そこら辺はVBもCもほとんど関係ないので。

で、VBにおける問題は「テクスチャをどのように準備するか?」です。
OpenGLではテクスチャのRGBAの値をそれぞれ1バイト・2バイト・4バイト整数のほか、Single型でも読み込ませることが出来ます。
とはいえRGBA各4バイトも取ってしまうと1ピクセル16バイトにもなってしまいますので、おそらくOpenGLが途中で変換しているのと思われます。(まぁ最近は浮動小数でRGBAを4バイトずつ取ることの出来るGPUも増えてきましたが)

普通は画像を準備する際、RGBはそれぞれ0-255の1バイトの値をとることが多いと思います。
そのため、ここではRGBAがそれぞれ1バイトのテクスチャを考えたいと思います。

OpenGLにテクスチャを渡す場合、各ピクセルは以下の様に配置されている必要があります。
  • テクスチャは下の行から順にメモリに格納される。
  • 各行は左から右のピクセルの順でメモリ中にRGBA値を格納する。
  • 各行は4バイトの倍数になるようにする。(RGBAに各1バイトを用いているのであれば気にする必要はない)
例えば100x100のテクスチャを準備する場合、各バイトは次の順に格納されます。
(0,99)のRGBA、(1,99)のRGBA、…、(99,99)のRGBA
(0,98)のRGBA、(1,98)のRGBA、…、(99,98)のRGBA



(0,0)のRGBA、(1,0)のRGBA、…、(99,0)のRGBA

これをみて何かピンと来る人もいるかもしれません。
よく見ると、WindowsのBMPファイルやDIBSection内の色要素の並び方と一緒です。

そのため、BMPファイルをOpenGLのテクスチャとして読み込むにはBMPファイルからDIBSectionを生成するAPIであるLoadImageを用いると非常にラクです。

OpenGLに読み込ませるテクスチャのデータをglTexImage2Dで送るわけですが、行列などと同様色のデータの先頭アドレスを送ればOKです。
glTexImage2Dの引数には、7つ目の引数にRGBAの要素が各何バイトかを指定する部分があります。
OpenGLのAPI宣言をするのに前回紹介したタイプライブラリを用いた場合、VBの補完機能で代入できる値の一覧が表示されると思います。
ここではglcByteとかglcLongとか出てくるので、
「RGBAそれぞれが1バイトだからglcByteだな」
と思ってglcByteにするとうまく行きません。

C言語の方をよく見ると、C言語では7つ目の引数に代入できる値にGL_UNSIGNED_BYTEとGL_BYTEがあります。
このタイプライブラリのglcByteの値はGL_BYTEと同じであり、符号付1バイト整数値を表すことになります。
VBのByte型は0〜255を表すということで符号付ではありませんので、これではうまく行きません。

このタイプライブラリではGL_UNSIGNED_BYTEに相当する値は定義されていないので、自分で定義するか直接対応する定数を書く必要があります。
ちなみにGL_UNSINGED_BYTEの値は5121です。


最後に

申し訳ないのですが最初にも書いたとおり、今回はあまり乗り気ではないことも会って手抜きな部分が多い。
どちらかというと「VBで3D描画をガリガリやりたい人向け」というよりは、「VBでOpenGLをやるときにちょっと注意する点」みたいな感じの説明文になってしまっている。

(プログラム講座にあるサンプルにある)プログラム作成自体は面白かったんだけど、説明となるとOpenGLプログラミングコースで十分説明されてしまっていることもあり気が乗らず。
ということで今回はVBならではの点を注意して見ました。
具体的にはここの説明を見るよりも、実際にコードを追ってみるかソースを見たほうがわかりやすいかも。

もっと自分が興味を持てるネタを扱わないとモチベーションが上がらないね。


一覧へ戻る