やっぱりCOMが好き

ローカルCOMサーバー活用法

VBだけで非同期処理を実現する



初音 玲 HATSUNE, Akira



Visual Basicの言語拡張の歴史は、順序だてて処理していたものを並行して処理できるようにする歴史でもある。このような、並行して処理し終わったら通知がくるような処理を非同期処理と呼ぶが、Windows DNAをはじめとする分散コンピューティングなどを考えるとそれは当然のことなのかもしれない。そして、元々Visual Basicはイベントドリブンといって画面上のコントロールなどがクリックされたらそれに対応したプロシージャが自動的に呼び出されるなど、並行処理した結果を受け取るための下地ができていたことも大きいだろう。

準備その1〜クラス
(サンプル1:Samp0704)

 非同期処理を理解する第一歩は、クラスモジュールの理解だ。クラスモジュールは、Visual Basicプロジェクトの他のモジュール(標準モジュールやフォームモジュール、クラスモジュール)からオブジェクトとして生成して使うことができるモジュールで、複数の同一ロジックを別々に管理できる。これは、クラスモジュールを使えば、並行処理の第一歩である“複数のロジックを別々に管理する”が実現できるということだ。前号の特集記事「クラスってなんだ!?」で使用したサンプルで、別々に管理する方法を確認する。
 クラスモジュールを使う場合、ひとつのクラスモジュール(clsSamp0704)に対して、

Set objSamp0704 = New clsSamp0704
のようにオブジェクト変数を割り当てるとクラスモジュールのロジックは、そのオブジェクト変数ごとに別々の変数領域が取られた状態で管理されることになる。この変数領域とは、プログラム中で宣言したり使ったりしている変数ばかりではなく、内部的な変数もすべて含んでいる。よって、クラスモジュールの中で管理している値を参照すると図1のようにオブジェクトごとに別々の値が返却されてくる。

図1:配列的クラスモジュール利用法
図1

 もちろん、クラスモジュールは変数領域こそ別々に管理されているが、他のモジュールから呼び出されると処理が終了するまでは呼び出し元モジュールに制御が戻ってこないので、クラスモジュールで非同期処理は実現できない。

準備その2〜インプロセスCOMサーバー
(サンプル2:Samp0802)

 クラスモジュールのオブジェクトを生成するときに、“非同期で動作するように生成する”ように指定できるのなら、クラスモジュールさえ理解していれば、非同期処理ができてしまうかもしれない。しかし、そうなってくると、標準モジュールで宣言されたPublic変数などをクラスモジュールから使おうとしたらどうなるだろうか。Public変数への参照や更新に対して排他がかけられるような機能があれば、排他待ちなどによりロックなどの機能が実現されているか、標準モジュールのPublic変数をクラスモジュールから直接読み書きできないようにする必要があるだろう。Visual Basic 4.0以降では、その命題に対してインプロセスCOMサーバー(ActiveX DLL)の作成を可能にすることで、標準モジュールのPublic変数を読み書きできない方法を実現した。
 インプロセスCOMサーバーを作るためにはクラスモジュールと異なり、標準EXE以外に別のVisual Basicプロジェクト(ActiveX DLLプロジェクト)が必要になる。Visual Basicプロジェクトが異なるということは、当然、標準EXEのVisual Basicプロジェクトに含まれる標準モジュールの中を参照できない。同時に、クラスモジュール単位でしかなかったオブジェクトが、Visual Basicプロジェクト単位のオブジェクトになるので、インプロセスCOMサーバーごとに別々の画面を表示/制御することが可能になる。

インプロセスCOMサーバーの作り方

 インプロセスCOMサーバーを作る方法は、クラスモジュールを作る方法によく似ている。ActiveX DLLの新規プロジェクトを作成すると標準モジュールの代わりにクラスモジュールがひとつだけのVisual Basicプロジェクトが作成されるので、サンプル1のクラスモジュールとまったく同じロジックを記述する(リスト1)。
リスト1:Samp0802.dllのソース
Option Explicit
Private mintCount As Integer
Public Function intCountDisp() As Integer
  mintCount = mintCount + 1
  intCountDisp = mintCount
End Function

クライアントの作り方

 インプロセスCOMサーバーのVisual Basicプロジェクトを保存したら、インプロセスCOMクライアント用の標準EXEを新規作成する。この標準EXE側で、インプロセスCOMサーバーを使う方法は、クラスモジュールの使い方と非常によく似ているが、多少の事前作業が必要だ。
 まず、標準EXEのVisual BasicプロジェクトにインプロセスCOMサーバーのVisual Basicプロジェクトを追加して、Visual Basicプロジェクトグループを作る(図2)。

図2:Visual Basicプロジェクトグループ
図2

そして、標準EXE側でインプロセスCOMサーバー側のVisual Basicプロジェクトを参照設定すれば、あとはリスト2のようにクラスモジュールを使うときとまったく同じだ(図3)。

図3:Visual Basicプロジェクトグループにおける参照設定
図3

リスト2:インプロセスCOMサーバーを使う
Option Explicit
' インプロセスCOMサーバーオブジェクト変数
Private maobjSamp(0 To 4)  As clsSamp0802
Private Sub Form_Initialize()
' インプロセスCOMサーバーオブジェクトの生成
  Set maobjSamp(0) = New clsSamp0802
  Set maobjSamp(1) = New clsSamp0802
  Set maobjSamp(2) = New clsSamp0802
  Set maobjSamp(3) = New clsSamp0802
  Set maobjSamp(4) = New clsSamp0802
End Sub
Private Sub Form_Terminate()
' インプロセスCOMサーバーオブジェクトの解放
  Set maobjSamp(0) = Nothing
  Set maobjSamp(1) = Nothing
  Set maobjSamp(2) = Nothing
  Set maobjSamp(3) = Nothing
  Set maobjSamp(4) = Nothing
End Sub
Private Sub cmdObjUse_Click(Index As Integer)
' [オブジェクトを使う]ボタン
  lblObjUse(Index).Caption = maobjSamp(Index).intCountDisp
End Sub

インプロセスCOMサーバーの動作

 もちろん、クラスモジュールと同様にインプロセスCOMサーバーでも変数領域は別々に管理される(図4)。

図4:インプロセスCOMサーバー
図4

それだけではなく、クライアントからインターフェイスポインタを直接呼び出せるので(図5)、ローディングさえ完了すれば、それ以降は、同一EXE内のクラスモジュール呼び出しとほぼ同じ速度で動作するのも特徴だ。


図5:インプロセスCOMサーバーの動作原理
図5

 また、インプロセスCOMサーバーへの参照設定は、実際はVisual BasicプロジェクトではなくActiveX DLLファイルに対して行なわれる。そのため、標準EXE(クライアント)のVisual BasicプロジェクトだけをIDEに読み込むと、インプロセスCOMサーバーのVisual Basicプロジェクト関連の情報をみることができなくなる(図6)。インプロセスCOMサーバーとは、まさにロジックを隠蔽したクラスモジュールといったところなのだ。

図6:Visual Basicプロジェクトにおける参照設定
図6
  1. クライアントは、CoCreateInstace-APIを使って、COMライブラリを呼び出す
  2. COMライブラリは、レジストリから指定されたCLSIDを検索して、該当ファイル使ってCOMサーバーのインスタンスを生成する
  3. COMライブラリを通して、指定されたインターフェイスAのポインタが返却される
  4. クライアントは、インターフェイスAを呼び出す
バージョン間の互換性を設定しておけばVisual Basicプロジェクトに対する参照設定はDLLに対する参照設定と解釈される 「プロジェクトの説明」が参照設定に表示される

COLUMN1 ActiveX DLLファイルの使い方
 開発環境でActiveX DLLを実行したとき、自動的に行なわれるためにあまり意識されないもののひとつにActiveX DLLファイルのレジストリへの登録がある。
 ActiveX DLLファイルを配布したときなどは、DOSプロンプトでActiveX DLLファイルのディレクトリに移動し、

REGSVR32 ActiveX DLLファイル名
としてレジストリへ登録する。レジストリには、ActiveX DLLファイルのディレクトリ位置も記憶されているので、もしActiveX DLLファイルを削除したりディレクトリを移動するときは、削除や移動前に、

REGSVR32 -u ActiveX DLLファイル名
としてレジストリからActiveX DLLファイルの情報を削除しなければならない。
また、ActiveX DLLファイルのバージョンを変更したときも同様な操作が必要だ。

準備その3〜ローカルCOMサーバー
(サンプル3:Samp0803)

 インプロセスCOMサーバーは、その名前が示すように呼び出し元と同じプロセスで動作する。しかし、Visual Basic 5.0のService Pack 2以降ではMTA(マルチスレッドアパートメント)と呼ばれるスレッドモデルがサポートされ(図7)、マルチスレッド対応のインプロセスCOMサーバーが作成できるようになっている。

図7:インプロセスCOMサーバーのスレッドモデル
図7

そのため、同じプロセス内であってもスレッドという最小の処理単位の管理領域が別々に確保されるので、複数のスレッドに分けて動作できる。しかしヘルプを読んでも明確には書かれていないが、Visual Basicには制限がある。Visual Basicで作成した標準EXEは、必ずSTA(シングルスレッドアパートメント)になるため、スレッドを分けてインプロセスCOMサーバーを生成できない。したがってVisual BasicだけではインプロセスCOMサーバーを使った非同期処理は実現できない。
 そこで、標準EXEのプロセスとは別プロセスで動作させることにより非同期処理を実現するしかないのだ。そのための仕組みがローカルCOMサーバー(ActiveX EXE)だ。言うなれば、マルチスレッドインプロセスCOMサーバーという言語拡張を行なう代わりに、ローカルCOMサーバーで一括して非同期処理は行なってしまおうということだ。
 本来、ローカルCOMサーバーは、同一プロセスにないため、ローカルCOMサーバーが異常終了しても呼び出し元が巻き添えを食わないようにする、その代わり使用に際しては処理時間のオーバーヘッドに目をつぶるというものだ。しかし、Visual Basicでは、処理速度や耐障害性という観点ではなく、非同期処理を実現するための唯一の方法という意味合いが強いだろう。

ローカルCOMサーバーの作り方

 ローカルCOMサーバーを作る方法は、インプロセスCOMサーバーを作る方法とよく似ている。ActiveX EXE新規プロジェクトを作成して、サンプル2のクラスモジュールとまったく同じロジック(リスト1)を記述しても良いし、サンプル2のActiveX DLLプロジェクトをコピーしてからActiveX EXEプロジェクトに変更するだけでも良い。

クライアントの作り方

 ローカルCOMサーバーのVisual Basicプロジェクトを保存したら、ローカルCOMクライアント用の標準EXE新規プロジェクトを作成する。この標準EXE側で、ローカルCOMサーバーを使う方法は、インプロセスCOMサーバーを使うときと区別がつかない。
 まず、標準EXEのVisual BasicプロジェクトにローカルCOMサーバーのVisual Basicプロジェクトを追加して、Visual Basicプロジェクトグループを作る。そして、標準EXE側でローカルCOMサーバー側のVisual Basicプロジェクトを参照設定すれば、あとはインプロセスCOMサーバーを使うときとまったく同じだ。詳細は、付録CD-ROMのサンプルSamp0803a.vbpをみてもらいたい。

ローカルCOMサーバーの動作

 ローカルCOMサーバーには、いくつかのスレッドモデルが存在し、Public変数の領域の管理方法が異なっている。
  1. オブジェクトごとのスレッド
    ローカルCOMサーバーのプロセス内にひとつのスレッドが新規生成されるモード。クライアントごとに別スレッドで動作することになるので、あたかもクライアントごとにローカルCOMサーバーが存在しているように使用できる。当然、このモードでは、Public変数の共有は行なわれない

  2. スレッドプール=1
    ローカルCOMサーバーのプロセス内の同じスレッドにオブジェクトが生成されるので、他のクライアントとPublic変数などの共有が可能だが、複数のクライアントからの処理を並行して処理することはできない

  3. スレッドプール 2以上
    指定された値までスレッドが生成されるので、もし指定値以下のクライアントしか存在しなければ、《1》の「オブジェクトごとのスレッド」と同等の動きとなる。指定値よりも多くのクライアントが存在するときは、ひとつのスレッドで処理するクライアントの数が増加することになる。つまり、限られた窓口で効率よく仕事をこなしてゆくようなイメージだ。よって、偶然にも同じスレッドを使うことになったCOMクライアント同士では、Public変数の共有が可能である

 ローカルCOMサーバーは、このようにPublic変数の柔軟性を利用して複数のEXE間で情報をやりとりできるだけではなく、プロセスも別々に管理されるので、ローカルCOMサーバーが異常終了しても、ローカルCOMクライアントは動作可能だ。
 このようにプロセスからみた動作はかなり異なっているインプロセスCOMサーバーとローカルCOMサーバーだが、COMの優れているところは、プロキシとスタブという考え方を導入して(図8)、ローカルCOMサーバーとインプロセスCOMサーバーを同等に扱えるようにした点だ。

図8:ローカルCOMサーバーの動作原理
図8
  1. クライアントは、CoCreateInstace APIを使って、COMライブラリを呼び出す
  2. COMライブラリは、レジストリから指定されたCLSIDを検索して、該当ファイル使ってCOMサーバーのプロセスを生成する
  3. COMサーバーのインターフェイスを模したプロキシを生成する
  4. COMライブラリを通して、指定されたインターフェイスAの(プロキシ上の)ポインタが返却される
  5. クライアントは、インターフェイスAを呼び出す
  6. プロキシは、ローカルプロシージャコールにより、スタブを呼び出す
  7. スタブは、インターフェイスAを呼び出す
もちろん、クライアントから呼び出すインターフェイスポインタは、プロキシのものであり、呼び出しごとのオーバーヘッドも生じるので、インプロセスCOMサーバーに比べて、ローディングも呼び出しも格段に遅くなる。

COLUMN2 ActiveX EXEファイルの使い方
 開発環境でActiveX EXEを実行したとき、自動的に行なわれるためにあまり意識されないもののひとつにActiveX EXEファイルのレジストリへの登録がある。
 ActiveX EXEファイルを配布したときなどは、DOSプロンプトでActiveX EXEファイルのディレクトリに移動し、

ActiveX EXEファイル名 /regserver
としてレジストリへ登録する。レジストリには、ActiveX EXEファイルのディレクトリ位置も記憶されているので、もし、ActiveX EXEファイルを削除したりディレクトリを移動するときは、削除や移動前に、

ActiveX EXEファイル名 /unregserver

としてレジストリからActiveX EXEファイルの情報を削除しなければならない。
 また、ActiveX EXEファイルのバージョンを変更したときも同様な操作が必要だ。


非同期通知を作成する
(サンプル4:Samp0804)

 処理の終了をクライアントに通知する方法は大別して2つある。ひとつは、処理状態を返却するプロパティを用意して、そのプロパティをクライアントから時々参照して終了を待つ方法だ。しかし、この方法では、ローカルCOMサーバーが処理中は、クライアントもプロパティを参照する無限ループを実行していることになり、無駄にCPUパワーを使ってしまうことになる。そこで、ローカルCOMサーバー側で処理が完了したら、クライアント側でイベントが発生するような仕組みが必要になってくる。イベント発生がサポートされれば無限ループと異なり、クライアント側で別の処理や操作が行なわれず、ローカルCOMサーバーの処理を待っているだけなので、CPUパワーを使わずに済む。
 ローカルCOMサーバーから非同期にクラアント側に通知を行なうためには、Visual Basic 5.0から追加されたWithEventsを使う。

ローカルCOMサーバー側

 ローカルCOMサーバー側では、外部に通知するイベントをPublic Eventとして定義し、そのEventに対して、RaiseEventを使ってイベントを発生させる(図9)。

図9:イベント
図9

 Samp0804.clsでは、クライアントからサブプロシージャが呼び出された直後と、Sleep APIを使って処理を一時停止した後の計2回のイベントを発生させている(リスト3)。

リスト3:ローカルCOMサーバー側で発生させるイベント
Option Explicit
Private Declare Sub Sleep Lib "kernel32" _
 (ByVal dwMilliseconds As Long)
Public Event evtSamp0804(ByVal vintCount As Integer)
Private mintCount As Integer
Public Sub psubCountDisp()
  Dim lngSleep    As Long
    
' (1)イベントを発生させる
  RaiseEvent evtSamp0804(0)

' (2)非同期処理を行なう
  mintCount = mintCount + 1

' (3)非同期動作を実感するために5秒処理を待つ
  lngSleep = 5000
  Sleep (lngSleep)

' (4)イベントを発生させる
  RaiseEvent evtSamp0804(mintCount)
End Sub

ローカルCOMクライアント側

 クライアント側では、ローカルCOMサーバーのオブジェクトを定義するときに、WithEvents付きで定義する。このとき、New修飾子を付けて、定義と同時にオブジェクトを生成してはいけない。
 その点さえ注意すれば、普通にローカルCOMサーバーを使うときとイベントを使うときの区別はない。もちろん、WithEvents付きで宣言することにより、

オブジェクト名_イベント名
というイベントに対応したサブプロシージャがクライアント側で利用可能となるので(図10)、そこにイベント発生時の処理を記述する(リスト4)。

図10:追加されたイベントプロシージャ
図10

リスト4:ローカルCOMクライアントに非同期通知をする
Option Explicit
Private Declare Function timeGetTime Lib "WINMM" () As Long
' ローカルCOMサーバーオブジェクト変数
Private WithEvents maobjSamp0  As clsSamp0804
Private WithEvents maobjSamp1  As clsSamp0804
Private WithEvents maobjSamp2  As clsSamp0804
Private WithEvents maobjSamp3  As clsSamp0804
Private WithEvents maobjSamp4  As clsSamp0804
Private Sub Form_Initialize()
' ローカルCOMサーバーオブジェクトの生成
  Set maobjSamp0 = New clsSamp0804
  Set maobjSamp1 = New clsSamp0804
  Set maobjSamp2 = New clsSamp0804
  Set maobjSamp3 = New clsSamp0804
  Set maobjSamp4 = New clsSamp0804
End Sub
Private Sub Form_Terminate()
' ローカルCOMサーバーオブジェクトの解放
  Set maobjSamp0 = Nothing
  Set maobjSamp1 = Nothing
  Set maobjSamp2 = Nothing
  Set maobjSamp3 = Nothing
  Set maobjSamp4 = Nothing
End Sub
Private Sub cmdObjUse_Click(Index As Integer)
' [イベントを使う]ボタン
  cmdObjUse(Index).Enabled = False
  Select Case Index
  Case 0
    Call maobjSamp0.psubCountDisp
  Case 1
    Call maobjSamp1.psubCountDisp
  Case 2
    Call maobjSamp2.psubCountDisp
  Case 3
    Call maobjSamp3.psubCountDisp
  Case 4
    Call maobjSamp4.psubCountDisp
  End Select
End Sub
Private Sub maobjSamp0_evtSamp0804(ByVal vintCount As Integer)
  lblObjUse(0).Caption = vintCount
  If vintCount > 0 Then
    cmdObjUse(0).Enabled = True
  End If
End Sub
Private Sub maobjSamp1_evtSamp0804(ByVal vintCount As Integer)
  lblObjUse(1).Caption = vintCount
  If vintCount > 0 Then
    cmdObjUse(1).Enabled = True
  End If
End Sub
Private Sub maobjSamp2_evtSamp0804(ByVal vintCount As Integer)
  lblObjUse(2).Caption = vintCount
  If vintCount > 0 Then
    cmdObjUse(2).Enabled = True
  End If
End Sub
Private Sub maobjSamp3_evtSamp0804(ByVal vintCount As Integer)
  lblObjUse(3).Caption = vintCount
  If vintCount > 0 Then
    cmdObjUse(3).Enabled = True
  End If
End Sub
Private Sub maobjSamp4_evtSamp0804(ByVal vintCount As Integer)
  lblObjUse(4).Caption = vintCount
  If vintCount > 0 Then
    cmdObjUse(4).Enabled = True
  End If
End Sub

実行の確認

 ローカルCOMサーバーとクライアントが同期的に動作していることを体感するためにはサンプルの動きを観察すると良い。
 サンプルを起動して、コマンドボタンを左クリックすると、
  1. コマンドボタンを使用不能にする
  2. 最初のイベントが発生する
  3. 処理の終了を待つ
  4. 処理終了のイベントが発生する
のように動作する(図11)。このことがまさしく同期的に動作することとイベントにより非同期通知が行なえる証拠といえるだろう。なぜなら、イベントを使って非同期通知が行なえないのならば、2つのイベントは、両方ともにサブプロシージャの処理が終了してクライアントに制御が戻ってから発生することになるからだ。

図11:イベントを使ったサンプル
図11

非同期処理を作成する
(サンプル5:Samp0805)

 サンプルの動作でもわかるように、ローカルCOMサーバーの機能は、COMが同期的に処理の橋渡しを行なってくれるので、あたかも普通にサブプロシージャを呼ぶのと同じ考え方で呼び出すことができる。これは非常に便利な機能で、ローカルCOMサーバーを別マシンに配置してDCOMサーバーとして利用するときにも適用され、複数マシンを連携して分散処理を可能としている。また、COMレベルでは同期的に処理しているが、プロセス的には別々に動作しているので、プロセス的には別プロセスである。したがって、長時間に渡る処理を行なっていてもクライアント側でフォームの再描画すら行なわれず、まるでフリーズしているかのように見えることもない。そのためクライアント側でタイマーを使ってプログレスバーや予定完了時間を変化させながらローカルCOMサーバーでの処理終了を待ち合わせることも可能だ。
 しかし、今回のように並列処理をさせたいときには、このCOMの仕様が非常に邪魔である。COM非同期呼び出しのようなオプションも存在しないので、プログラム側で工夫をしなければならない。

タイマーの利用

 並列処理を行なうためには、COMで呼び出されたサブプロシージャの中ではほとんど処理を行なわず直ちにクライアントに復帰し、ローカルCOMサーバー側では別の契機により実際の処理を行なうようにしなければならない。具体的には、クライアントから呼び出されたサブプロシージャでは、タイマーを起動するだけで直ちにクライアントに復帰して、タイマーイベントの中で実際の処理と終了イベント発行を行なうという流れだ(図12・13)。

図12:非同期処理を使ったサンプル
図12

図13:非同期処理の流れ
図13

 タイマーを利用するためには、ローカルCOMサーバーにフォームオブジェクトを追加してタイマーコントロールを使う方法もあるが、SetTimer APIを使い標準モジュールだけで実現する方法もある(リスト5・6)。

リスト5:clsSamp0805(Samp0805.cls)
Option Explicit
Public Event evtSamp0805(ByVal vintCount As Integer)
Public Sub psubCountDisp()
' (1)イベントを発生させる
  RaiseEvent evtSamp0805(0)
    
' (2)タイマーを起動する
  Call psubBeginTimer(Me)
End Sub
Friend Sub Notify(ByVal vintCount As Integer)
' (9)終了イベントを発生させる
  RaiseEvent evtSamp0805(vintCount)
End Sub
Public Sub StopTimer()
  Call psubEndTimer
End Sub
Private Sub Class_Terminate()
  ' タイマーが停止していることを確認
  Call psubEndTimer
End Sub

リスト6:basSamp0805b(Samp0805b.bas)
Option Explicit
Private Declare Function SetTimer Lib "user32" _
                      (ByVal hwnd As Long, _
                       ByVal nIDEvent As Long, _
                       ByVal uElapse As Long, _
                       ByVal lpTimerFunc As Long) As Long
Private Declare Function KillTimer Lib "user32" _
                      (ByVal hwnd As Long, _
                       ByVal nIDEvent As Long) As Long
Private Declare Sub Sleep Lib "kernel32" _
                      (ByVal dwMilliseconds As Long)
Private mlngTimerID As Long
Private mobjSamp0805 As clsSamp0805
Private mintCount As Integer
Public Sub psubEndTimer()
  Dim lngRet  As Long
    
  lngRet = KillTimer(0, mlngTimerID)
End Sub
Public Sub psubBeginTimer(ByRef robjSamp0805 As clsSamp0805)
' (3)クラスモジュールへの参照を保存
  Set mobjSamp0805 = robjSamp0805

' (4)タイマーをセットして、とりあえず処理終了
  mlngTimerID = SetTimer(0, 0, 1, AddressOf subTimerProc)
End Sub
Private Sub subTimerProc(ByVal hwnd As Long, _
                        ByVal umsg As Long, _
                        ByVal idEvent As Long, _
                        ByVal dwtime As Long)
  Dim lngSleep    As Long
  Dim lngRet  As Long

' (5)タイマーを停止
  lngRet = KillTimer(0, mlngTimerID)

' (6)非同期処理を行なう
  mintCount = mintCount + 1

' (7)非同期動作を実感するために5秒処理を待つ
  lngSleep = 5000
  Sleep (lngSleep)
    
'(8)処理完了通知
  mobjSamp0805.Notify (mintCount)
End Sub

 SetTimer APIは、第4パラメータにタイムアウト時に呼び出すプロシージャを指定できるので、AddressOfを使ってsubTimerProcの先頭アドレスを指定している。このsubTimerProcの中に実際に処理するロジックを記述してゆくことになる。

ローカルCOMサーバーの設定

 ローカルCOMサーバーのインスタンシングは、オブジェクトごとのスレッドにして、標準モジュール内のPublic変数などがオブジェクトごとに別管理されるように設定する。

COLUMN3 IDE上で実行時の注意点
 ローカルCOMサーバーのインスタンシングに「オブジェクトごとのスレッド」を指定していたとしてもActiveX EXEとバイナリ互換性のあるVisual Basicプロジェクトを実行してデバッグしているときは、IDEのスレッドしか起動されない。したがって、オブジェクトごとのスレッドにはならないので注意が必要だ。


非同期処理を隠蔽する
(サンプル6:Samp0806)

 ローカルCOMサーバーは、オブジェクトの生成さえ終わってしまえば、普通のサブプロシージャを呼び出すのと同様のプログラミングで利用できる。しかし、オブジェクトの生成やそのためのオブジェクト変数の定義、参照設定の実施など“簡単に使う”には多少の知識が必要になってくる。そこで、ユーザーコントロールを作成し、ローカルCOMサーバーの呼び出しを隠蔽することで、「オブジェクト」ということを意識しないで非同期処理を実現する。

ユーザーコントロールの準備

 処理を隠蔽するコントロールは、タイマーコントロールなどと同じように実行時は非表示になるコントロールにするのが良いだろう。実行時非表示コントロールを作成するには、新規プロジェクトの作成でActiveXコントロールを選択したら、InvisibleAtRunTImeプロパティをFalseすればよい。また、ツールボックスに表示するアイコンをToolboxBitmapプロパティに設定し、設計時のフォームで表示されるアイコンをPictureプロパティに設定する。

隠蔽するロジックの記述

 ユーザーコントロールに隠蔽するロジックは、次の4つだ(リスト7)。

リスト7:ctlSamp0806a(Samp0806a.ctl)
Option Explicit
' ローカルCOMサーバーオブジェクト変数
Private WithEvents maobjSamp  As clsSamp0805
' 公開メソッド
Public Event evtSamp0806(ByVal vintCount As Integer)
Private Sub maobjSamp_evtSamp0805(ByVal vintCount As Integer)
' 公開イベントの発行
  RaiseEvent evtSamp0806(vintCount)
End Sub

Private Sub UserControl_Initialize()
' ローカルCOMサーバーオブジェクトの生成
    Set maobjSamp = New clsSamp0805
End Sub
Private Sub UserControl_Terminate()
' ローカルCOMサーバーオブジェクトの解放
  Set maobjSamp = Nothing
End Sub
Public Sub psubCountDisp()
' 公開メソッド
' 非同期処理の実行
  Call maobjSamp.psubCountDisp
End Sub
  1. ローカルCOMサーバーの生成
    UserControl_InitializeイベントでローカルCOMサーバーを生成する
  2. ローカルCOMサーバーの呼び出し
    ローカルCOMサーバーに実行させるロジックをユーザーコントロールのメソッドとして公開するために、Publicサブプロシージャを定義して、その中でローカルCOMサーバーを呼び出す
  3. イベントの公開
    Public Eventを使ってユーザーコントロールの公開イベントを定義する。そして、ローカルCOMサーバーからのイベントが発生したら、その情報を引き継いでRaiseEventによりユーザーコントロールのイベントを発生させる
  4. ローカルCOMサーバーの解放
    UserControl_TerminateイベントでローカルCOMサーバーを解放する

画面の作成

 サンプル5(Samp0805)と同様の画面を作成して、ユーザーコントロールを貼り付ける(図14)。そして、ローカルCOMサーバーのメソッドとイベントの代わりに、コントロールのメソッドとイベントとしてプログラムする(リスト8)。

図14:ユーザーコントロールの利用
図14

リスト8:frmSamp0806(Samp0806.frm)
Option Explicit
Private Declare Function timeGetTime Lib "WINMM" () As Long
Private Sub cmdObjUse_Click(Index As Integer)
' [イベントを使う]ボタン
  cmdObjUse(Index).Enabled = False
  utlSamp(Index).psubCountDisp
End Sub
Private Sub utlSamp_evtSamp0806(Index As Integer, ByVal vintCount As Integer)
  lblObjUse(Index).Caption = vintCount
  If vintCount > 0 Then
    cmdObjUse(Index).Enabled = True
  End If
End Sub

 ユーザーコントロール化したときの恩恵としては、ロジックの隠蔽以外にもコントロール配列化することにより今回のサンプルのようなパターンでは極めてロジックが簡素になることも挙げられる。もちろん、WithEventsを指定したオブジェクト変数でもイベントサブプロシージャの引数としてIndexパラメータがあれば十分実現可能だ。しかし、Visual Basic 6.0ではまだそこまで対応できていないので、コントロール配列というアプローチは重宝することが多い。

最後に

 今回、紹介した方法を応用してゆくと、既存コンポーネントをローカルCOMサーバーで呼び出し、そのローカルCOMサーバーをユーザーコントロールで隠蔽することで、あらゆるものを非同期処理できるようになるだろう。もちろん、それだけではなく、非同期ローカルCOMサーバーや非同期DCOMサーバーによるビジネスロジックの非同期化という手法により、クライアント側があたかもフリーズしているように見えることを防止しつつ複雑な処理を実行することもできる。応用範囲は多岐に渡るといえるだろう。

□稼動確認環境
Windows 98 SE 4.10.2222a
Visual Basic 6.0(SP3)


VB Magazine ライブラリ| Visual Basic WorkGroup
int21 ホームページ| PCDN ホームページ

PCDN
Copyright (c) 1998 int21 CorporationAll Rights Reserved.
For questions or comments, please send mail to: pcdn@int21.co.jp