ActiveXコントロール 開発Tips

    酒井法雄 SAKAI, Norio   


    Visual Basic 5.0の新機能の中でも,やりはじめると楽しいのがActiveXコントロールの作成だ.以前の記事で何度か書いてきたように,Wizardを使って比較的カンタンに作ることができ,部品化を推し進めることができるからだ.しかし,いざVisual Basicで再利用できるような部品を作ろうとすると,ひっかかることも少なくない.ここでは,そうしたActiveXコントロール作成に関するTipsを紹介しよう.

    固定・最小サイズを実現するには

     Timerコントロールのように固定サイズのActiveXコントロールを実現したいことがある.また,あるサイズ以下にならないようにしたい,あるいはその逆にあるサイズ以上にならないようにしたいときがある.

    ふつうのフォームなら…
     普通のフォームでこれを実現しようとするなら,最初からBorderStyleプロパティを「1- 固定 (実線)」にしてデザインするのが普通だ.もちろん,最小サイズや最大サイズを規定しようと思えば,この手は使えない.普通に「2- 可変」にした上で,Resizeイベントに次にようなコードを書くことになるだろう(リスト1-a).
     このコードでは,ピクセル単位を元にして,ScreenオブジェクトのScreen.TwipsPerPixelXおよびScreen.TwipsPerPixelYでTwipに変更している.
     しかし,これはあまりよいコードとはいえない.というのは,Widthプロパティを変更した時点で,さらに Form_Resizeイベントが発生してしまうからだ.この様子は,If文にブレークポイントを指定し,実行してみれば,複数回呼び出されていることがわかる.そこでCtrl+を押して呼び出し履歴を表示してみれば,一目瞭然だ.

    図1:呼び出し履歴

     この問題を回避し,しかもさらにカンタンに1行で書こうとするならば,リスト1-bのようになる.

    リスト1
    ■リスト1-a Private Sub Form_Resize()   If Me.Width > 200 * Screen.TwipsPerPixelX Or Me.Height > 150 * Screen.TwipsPerPixelY Then      Me.Width = 200 * Screen.TwipsPerPixelX      Me.Height = 150 * Screen.TwipsPerPixelY   End If End Sub■リスト1-b Private Sub Form_Resize()   If Me.Width > 200 * Screen.TwipsPerPixelX Or Me.Height > 150 * Screen.TwipsPerPixelY Then      Me.Move Me.Left, Me.Top, 200 * Screen.TwipsPerPixelX, 150 * Screen.TwipsPerPixelY   End If End Sub

     ところが,同様に呼び出し履歴を出してみると,起動時には確かに一回少なくなるが,実行時にはやり2回呼び出されることがわかる.これは操作の問題ではない.試しにコードで変更しても同じである.実は,一回目は変更されたサイズにされて発生し,2回目は一回目に呼び出されて修正された値で発生するのである.
     したがって,起動時以外のときには WidthとHeightを別々に変更しても影響はないということである.とはいえ,後者の方が一行で済むからスマートである.

    ActiveXコントロールではそうはいかない
     さて,ActiveXコントロールでも同様の手法が考えられる.UserControlモジュールにもResizeイベントがあるから,同様のコードを書いてみよう.しかし,これは実行しようとすると,「メソッドまたはデータメンバーが見つかりません」というエラーになってしまう.
     これは,MeすなわちUserControlオブジェクトには,WidthとHeightというメソッドがないからなのである.ActiveXコントロールのサイズを規定するのは,そのコントロールを使っているツール,すなわちVisual Basicなどの開発環境側である.このような値は,Extenderオブジェクトに含められている.したがって,MeをExtenderに置換した次のコードであれば動作する(リスト2-a).
     その前の例にあったようなWidthやHeightをExtenerオブジェクトのプロパティとして変更してやると,かなり動きがおかしくなるが,このコードはデザイン時にリサイズすれば,だいたい動作する.
     しかし,コントロールの左または上方向にリサイズしたとき,きちんとしたサイズにならない,あるいは移動してしまうことがある.これは,わざわざこのActiveXコントロールを使っている親にサイズを変更させている,といったあたりに問題がありそうだ.

    Sizeメソッドを使う
     では,どうしたらよいだろうか? 実は,最適なメソッドが最初から用意されているのである.それは,Sizeメソッドである.このメソッドは,

    object.Size width, height

    という使い方をするごくシンプルなものである.すなわち,リスト2-bのようなコードにすればよい.

    リスト2
    ■リスト2-a If Extender.Width > 200 * Screen.TwipsPerPixelX Or Extender.Height > 150 * Screen.TwipsPerPixelY Then   Extender.Move Extender.Left, Extender.Top, 200 * Screen.TwipsPerPixelX, 150 * Screen.TwipsPerPixelY End If■リスト2-b Private Sub UserControl_Resize()   If Extender.Width > 200 * Screen.TwipsPerPixelX Or Extender.Height > 150 * Screen.TwipsPerPixelY Then     Size 200 * Screen.TwipsPerPixelX, 150 * Screen.TwipsPerPixelY   End If End Sub

     実際に使ってみると,先ほど指摘したような問題はこのコードでも再現する.これはVisual Basicのバグだろう.しかし,これはデザイン時のオペレーションで回避できる問題なので,クリティカルなものではない.

    サンプル ---キャプションのサイズになるActiveXコントロール
     Sizeメソッドを使ったサンプルとして自動的にキャプションのサイズになるActiveXコントロール「MiniSize」を作成した.話はカンタンで,AutoSizeをTrueにしたラベルを用意しておき,ResizeイベントでSizeメソッドをうまく使うのである(図2・リスト3).

    図2:MiniSize設計画面と実行例

    リスト3
    Private Sub UserControl_Resize()  If Width < lblX.Width Or Height < lblX.Height Then    Size lblX.Width, lblX.Height  End IfEnd Sub

    この例では,コントロールはラベルのキャプションに合わせたサイズ以下にならないようになっている.フォーム上に配置されたボタンを押すとActiveXの公開されたCaptionプロパティが変更される.すると,コントロール自体がラベルに表示された文字列サイズに合わせられる.MoveメソッドやWidth/Heightプロパティでは正常に動作しないので注意が必要だ.

    実行時に見えないコントロールにするには

     Timerコントロールなどのように,実行時に見えないコントロールとしたいことがある.このようなときには,Ambient.UserModeなどで判断せず,単純にInvisibleAtRunTimeプロパティをTrueにすればよい.

    ツールボックスビットマップ

     Visual Basicで作成したActiveXコントロールは,Visual Basicのツールボックス上にはデフォルトで図のようなアイコンとして表示される.

     これを変更するには,ToolboxBitmapプロパティにあらかじめ用意した16×15ピクセルのビットマップを読み込んでおけばよい.実際には,BMP,DIB,PAL,GIF,JPGをサポートしている.ICO形式は読めないので注意してほしい.

     さらに,このビットマップは大きなものであっても自動的にリサイズされる.ActiveXコントロールプロジェクト内には縮小されたサイズのものが入るので,大きさを心配する必要はない.次の例は,クルマの写真を取り込んだものだが,よく分からないものの,ちゃんとアイコンとして表示されていることがわかるだろう.

    Alignプロパティを実現するには

     Dataコントロールのように,フォーム上の上下左右のいずれかに揃えて貼りつけたいことがある.DataコントロールではこれをAlignプロパティとして実現している(図3-a).
     Alignプロパティを実現するのはカンタンだ.特にコードを書く必要はなく,UserControlモジュールのAlignableプロパティをTrueに設定すればよい.ただし,このときにドロップタウンされるリストはVisual Basicの定数リストになる(図3-b).
    なお,ResizeイベントでSizeメソッドを使ってリサイズした値が優先されるので,上揃えのAlignにしても,幅が足りないといったことが起きうる.このようなときには,幅や高さなどがおかしくならないようにコーディングする必要がある.

    図3-a:Alignプロパティ
    図3-a

    Drag&DropサーバーActiveXコントロールを実現するには

     作成したActiveXコントロール上にあるリストから,選んだものだけをDrag&Dropして,フォーム上の他のリストボックスにコピーしたいことがある.

    ふつうのフォームなら…
     通常のフォーム上でのDrag&Dropを使うならば,次のような手順で実現することになるだろう.

    1. MouseDownイベントでラベルなどのダミー枠をドラッグ開始
    2. ドロップ先リストボックスのDragDropイベントで,ソースで選択された内容を得て,その内容をリストボックスにコピーする
     文字で書くとカンタンだが,実際にはダミーとなるラベルなどのコントロールをドラッグし,内容をいちいち調べなくてはならない.これは決してカンタンなことではない.
     ここで注意して欲しいのは,単純にソース側のリストボックスのDragModeを自動にしてはいけないということだ.この設定では,リストボックス全体がドラッグされてしまうのである.

    ActiveXコントロールではそうはいかない
     では,ActiveXコントロールでも同様にすればよいと思うわけだが,実はそうはいかない.ソースとなるリストボックスはActiveXコントロール内部で使われているものであり,別のプログラムなのである.Visual Basicレベルで実現されているDragメソッドやDragDropイベントは同じプログラム内で動作するためのものであるから,ドロップできないのである.

    OLEのDrag&Dropを使おう
     そこで出てくるのが,OLEである.OLEには,あらかじめDrag&Dropの仕様が用意されている.Visual Basic 5.0からは,Visual Basicでもこの仕組みを使うことができるようになっている.以前の特集や長谷川氏の記事でもこのことは述べられているので,ここでは詳しくは説明しない.
     OLEでのDrag&Dropは,そもそもが別のプログラム間でデータをやり取りしようというものであるから,ActiveXコントロールがDrag&Dropサーバーとなることができるのである.しかも,大変カンタンだ.
     ここでは,ソースとなるActiveXコントロールにリストボックスlistSrcを配置した.そして,OLEDragModeをデザイン時に自動に設定しただけだ.

    listSrc.OLEDragMode = 1 - 自動

    たったこれだけである.一応,テストができるようにリストボックスに適当なデータを入れておくために,次のようなコードを記述した.

    Private Sub UserControl_Initialize()  Dim i As Integer      For i = 0 To 10    lstSrc.AddItem "test" & CStr(i)  Next iEnd Sub

     むしろ,問題はドロップされた先である.このActiveXコントロールを貼りつけたフォーム上に,リストボックスList1を配置した.List1のOLEDropModeは,デザイン時に手動とし,OLEDragDropイベントが発生するようにする.

    List1.OLEDropMode = 1 - 手動

     ActiveXコントロール側からリストボックスの内容が選択され,List1にDrag&Dropされると,OLEDragDropイベントが発生する. ここには,リスト4のようなコードを記述する.

    リスト4:
    Private Sub List1_OLEDragDrop _  (Data As DataObject, Effect As Long, Button As Integer, _  Shift As Integer, X As Single, Y As Single)  Dim i As Integer  Dim vList As Variant  Dim sItem As String  Dim st As Integer      vList = Data.GetData(vbCFText)  st = 1  For i = 1 To Len(vList) + 1    If Mid(vList, i, 1) = vbCr Or i = Len(vList) + 1 Then      sItem = Mid(vList, st, i - st)      List1.AddItem sItem      st = i + 2    End If  Next iEnd Sub

     DataObject型の引数Dataには,Drag&Dropされたソースの内容が入っている.ここからGetDataメソッドを使って必要なデータを取り出し,Variant型変数vListに格納する.このときドロップされる内容にはさまざまなデータ型がありえるわけだ.この型は引数として指定する.ふつうのリストボックスからのDrag&Dropでは,テキスト型(vbCFText)を指定すればよい.
     ヘルプなどには,引数を省略するか0を指定すれば適切なデータ型が入るとあるが,これはウソでエラーになってしまう.したがって,きっちりと合致するデータ型を指定する必要がある.なお,ファイルリストボックスからのDrag&Dropも試してみたが,vbCFFilesにすると型は合っているのにエラーになってしまった.他の型では型が違うというエラーになってしまう.このあたりの動作はいまひとつ整合性が取れていないようだ.
     VListの内容を調べてみると,DropされたそれぞれのアイテムはCR+LF区切りで連結されている.したがって,vbCRをデリミタとして抜き出すことができる.
     これで通常のリストボックスを元にしたActiveXコントロールからのDrag&Dropのでき上がりである(図4).このように,Drag&Dropサーバーとすることはきわめて容易だが,取り出す部分のコードが煩雑な上,不整合もある.この部分までをうまくカプセル化するためには,Drop側もActiveXコントロールとする必要があるかもしれない.

    図4:サンプルプログラムDrag

    非同期でピクチャー/ファイルを転送するには

     ActiveXコントロールやActiveX Documentsでは,コントロールやドキュメントとしてプログラムがダウンロードされる.リモートにあるプログラムがそのまま実行されるのだから,これは痛快なことである.しかし,ダウンロードされた後にそのプログラムのデータとしてピクチャーやファイルなどを使いたい場面もある.
     このようなときに便利なのが,非同期ピクチャー/ファイル転送機能である.これは,ActiveXコントロールだけでなく,ActiveX Documentsでも使える機能だ.これを使えば,ActiveXコントロールが動作してから,指定したURLからピクチャーファイルなどを非同期でダウンロードして表示することができる.また,あらゆる形式のファイルも非同期でダウンロードし,ファイルとしてローカルにコピーすることができる.
     非同期であるから,ダウンロード中は他の処理を継続することができる.処理は「ダウンロードの開始」と「ダウンロードの終了」に分けて考えることができる.

    ダウンロードの開始
     AsyncReadメソッドを使ってダウンロードを開始する.このメソッドの概要を表1に示す.

    表1:AsyncReadメソッド
    構文:object.AsyncRead Target, AsyncType [, PropertyName]
    指定項目内容
    object「対象」一覧内のいずれかのオブジェクトを指すオブジェクト式
    Target データが存在している場所を表わす文字列式を指定する.パス名またはURLを指定できる
    AsyncTypeデータの形式を表わす長整数型を指定する.設定する値については,次の「設定値」を参照
    PropertyName省略可能.ロードされるプロパティの名前を表わす文字列式を指定する
    引数AsyncTypeの設定値
    定数内容
    vbAsyncTypeFileデータは,Visual Basicが作成したファイル
    vbAsyncTypeByteArrayデータは,取り出されたデータが入っているバイト配列.コントロール側でデータの扱い方がわかっていることを想定している
    vbAsyncTypePictureデータは,Pictureオブジェクト

    このように,パスまたはURLおよび,ファイル,バイト配列,ピクチャーのいずれか,ロードされるプロパティの名前を指定する.ピクチャーであれば,次のように指定することができる.
    AsyncRead _"http://www.int21.ne.jp/icons/int21logo.gif", _vbAsyncTypePicture, "PicDownload"

     実際には,デザイン時には実行されないようするなどのコードを記述することも必要だ.ここでは,PicDownloadプロパティにURLを指定されると,ピクチャーの非同期転送を開始するコードを次のように記述した(リスト5-a).また,ファイル名のURLを指定されると,ファイルの非同期転送を開始するコードを次のように記述した(リスト5-b).

    リスト5
    ■リスト5-a:ピクチャーの非同期手相を開始' Specify Download FilePublic Property Let PicDownload(ByVal New_PicDownload As String)  If Ambient.UserMode = False Then Err.Raise 382  m_PicDownload = New_PicDownload  If Ambient.UserMode = True And New_PicDownload <> "" Then    AsyncRead New_PicDownload, vbAsyncTypePicture, "PicDownload"  End If  PropertyChanged "PicDownload"End Property■リスト5-b:ファイルの非同期手相を開始Public Property Let FileDownload(ByVal New_FileDownload As String)  If Ambient.UserMode = False Then Err.Raise 382  m_FileDownload = New_FileDownload      If Ambient.UserMode = True And New_FileDownload <> "" Then    AsyncRead New_FileDownload, vbAsyncTypeFile, "FileDownload"  End If  PropertyChanged "FileDownload"End Property

    ダウンロードの終了
     ダウンロードの終了は,AsyncReadCompleteイベントで知ることができる.次に概要を示す.

    表2:AsyncReadCompleteメソッド
    構文:Sub object_AsyncReadComplete(PropertyValue As AsyncProperty)
    指定項目内容
    object「対象」一覧内のいずれかのオブジェクトを指すオブジェクト式
    PropertyValue次のプロパティが入っている AsyncPropertyオブジェクト
    Value非同期読み出しの結果を示すバリアント型 (Variant) の値.既定のプロパティ
    PropertyNameAsyncReadメソッドで渡されたプロパティ名を表わす文字列
    AsyncTypeValueプロパティのデータ型を表わす整数値.設定される値については,次の「設定値」を参照
    引数AsyncTypeの設定値
    定数内容
    vbAsyncTypeFile Valueプロパティは,データが入っている一時ファイルのパス名を表わす文字列
    vbAsyncTypeByteArrayValueプロパティは,データが入っているバイト配列
    vbAsyncTypePictureValueプロパティは,正しいフォーマットのピクチャーオブジェクト

     イベントの引数AsyncProperty型オブジェクトAsyncPropのPropertyNameプロパティで,ダウンロードが終了したプロパティ名を知ることができる.
     ピクチャーであれば,AsyncProp.Valueプロパティがピクチャーの内容そのものになる.これを表示したいオブジェクトのPictureプロパティにSetすればよい.
     ファイルであれば,AsyncProp.Valueプロパティはダウンロードされたパス名(テンポラリファイル名)になる.そのファイルをどういじろうが,まったくの自由である.ここでは,なんとEXEファイルを非同期転送して,ローカルでいきなり動かしてしまうというコードを書いてみた(リスト6).

    リスト6
    ■リスト6-a:サンプルプログラムAsyncRead' AsyncReadComplete EventPrivate Sub UserControl_AsyncReadComplete(AsyncProp As AsyncProperty)  Dim i As Integer  Dim s As String    ' On Error Resume Next  Select Case AsyncProp.PropertyName  Case "PicDownload"    On Error Resume Next    Set Picture = AsyncProp.Value    If Err Then MsgBox Err.Description, vbCritical, _     "Error" & CStr(Err.Number)      On Error GoTo 0      Debug.Print "Download Complete"  Case "FileDownload"    On Error Resume Next    s = AsyncProp.Value    If Err Then MsgBox Err.Description, vbCritical, _     "Error" & CStr(Err.Number)    On Error GoTo 0    For i = Len(s) To 1 Step -1      If Mid(s, i, 1) = "." Then        s = Left(s, i)          Exit For      End If    Next i    On Error Resume Next    Kill s & EXECFILE    Name AsyncProp.Value As s & EXECFILE    Debug.Print "Download Complete", s & EXECFILE    Shell s & EXECFILE, vbNormalFocus  End SelectEnd Sub■リスト6-b:コンテナ側Private Const DOWNLOADFILE = _  "http://pcdn.int21.co.jp/pcdn/pcdnlink.gif"Private Const DOWNLOADEXE = _ "http://pcdn.int21.co.jp/pcdn/misc/picview.exe"Private Sub Command1_Click()    AsPic1.PicDownload = DOWNLOADFILEEnd SubPrivate Sub Command2_Click()    AsPic1.FileDownload = DOWNLOADEXE End Sub

     コンテナ側には,次のようなコードを書いてみた(リスト6-b).実行画面を次に示す(図5).ボタンを押すと,指定したURLからGIFファイルがダウンロードされる.また,Visual Basic 5.0で作成した便利なピクチャービュワーもダウンロードできる.

    図5:AsyncRead実行画面

     このActiveXコントロールを元にしてインターネットセットアップを作成し,多少HTML + VBScriptを追加したコードと実行画面を示す(リスト7・図6).
     ここでは自動的にちょっとびっくりするようなEXEファイルがダウンロード,実行されるようになっている.このようなことができてしまうというのは,きわめて危険である.どこに悪意があるともわからないインターネットでは,やはりActiveXコントロールやActiveX Documentsはむやみに実行しない方がよいことがわかるだろう.だが,一概に否定すべきものではない.危険な反面,それだけ自由度が高いわけで,セキュリティに不安要素の少ないイントラネットなどの環境で使うには,非常に便利な機能である.

    リスト7:
    <HTML><TITLE>非同期ピクチャー/ファイル転送ActiveXコントロールのテスト</TITLE><SCRIPT LANGUAGE="VBScript"><!--Sub window_onLoad()    AsPic.PicDownload = "http://pcdn.int21.co.jp/pcdn/pcdnlink.gif"    AsPic.FileDownLoad = "http://pcdn.int21.co.jp/pcdn/misc/wh1.exe"end subSub cmdRead_OnClick()     AsPic.PicDownload = txtURL.ValueEnd SubSub cmdExec_OnClick()     AsPic.FileDownload = txtExec.ValueEnd Sub--></SCRIPT><HEAD><H1>非同期ピクチャー/ファイル転送ActiveXコントロールのテスト</H1><H3>酒井 法雄</H3></HEAD><BODY><P><HR>VB5で作成したActiveXコントロールで,非同期にピクチャーやファイルを転送するテストです.<BR><UL><LI>起動時,非同期にピクチャーを適当なURLから転送し,下のグラフに表示します.<BR><LI>起動時,非同期にEXEファイル(VB4で作ったオモチャプログラム)を転送し,転送終了後すぐに実行されますが,画面には5分後に出てきます.<BR><LI>Picture URLに適切なピクチャーファイルURLを指定して,右側のボタンを押すと,その画面のまま非同期にピクチャーが読み込まれます.<BR><LI>FILE URLに適切なEXEファイルURLを指定し,右側のボタンを押すと,その画面のまま非同期にEXEファイルがダウンロードされ,転送終了後に実行されます.<BR></UL><P>ここからも分かるように,後から非同期に転送されたときには,何の確認もありません.<BR>このようにActiveXコントロールは危険なことができますので,むやみに実行しないよう気をつけましょう.<BR><P><HR><P>Picture URL:<INPUT TYPE="text" NAME="txtURL" VALUE="http://www.int21.co.jp/icons/pcdn_sh_310.gif" SIZE=50><INPUT TYPE="BUTTON" NAME ="cmdRead" VALUE="非同期読込">  <P>FILE URL: <INPUT TYPE="text" NAME="txtExec" VALUE="http://pcdn.int21.co.jp/pcdn/misc/pview.exe" SIZE=50><INPUT TYPE="BUTTON" NAME ="cmdExec" VALUE="非同期ファイル転送">  <P><OBJECT ID="AsPic" WIDTH=263 HEIGHT=157CLASSID="CLSID:789901F9-C096-11D0-9B4F-0040C79407A8"CODEBASE="asyncpic.cab#version=1,0,0,0"></OBJECT><P><HR><P>注意<BR><UL><LI>テンポラリディレクトリにテンポラリファイル名でEXEが作られます.<BR><LI>Temporary Internet Filesディレクトリには,元々の名前付きでEXEファイルがあります.<BR><LI>起動時に転送される wh1.exe はVisual Basic 4.0で作られているので,ランタイムライブラリがないと動きません.<BR></UL></BODY></HTML>

    図6:

    コントロールの背景を透明にするには

     最近では,デジタルカメラの普及もあり,自分で撮った写真をコンピュータで画像を処理するといったことも手軽にできるようになってきた.画像を加工するソフトならば,複数の画像を組み合わせて,ピクチャーの回りをハート型でくりぬくなどということも朝飯前だ.さらにいえば,ハート型の外側を透明にして,下の絵に重ねるなどということもできる.このようなことをVisual Basicでもしたいものだ.
     従来であれば,BitBltなどのWindows APIを駆使してこういったことを実現したものだが,これはなかなか手ごわいものだった.しかし,実はActiveXコントロールにしてしまえば,もっとカンタンに実現することができるのである.
     ActiveXコントロールでは,UserControlオブジェクトのBackStyleプロパティを「0 - 透明」に指定すれば,背景を透明に設定することができる.さらに,Picture,MaskPictureとMaskColorの組み合わせで,ピクチャーの特定の色を透明色にすることもできる.
     これらの機能を使えば,文字以外が透明なラベルや,一部が透明なピクチャー,さらには不定形のコントロールも実現することができる.次に,それぞれについて述べよう.

    図7:MaskLbl

    文字以外が透明なラベル
     まずは,比較的カンタンな例として,透明なラベルを作ってみよう(図7).
     実は,ラベルコントロールにも BackStyleプロパティがあり,「0 - 透明」にすれば,透明なラベルができてしまう.しかしこれだけでは意味がない.実際にはスクロールする透明なラベルを作りたいといったラベル+αの機能を持ったActiveXコントロールを作るという話になるだろう.もちろん,何も難しいことはない.UserControlオブジェクトとその上に配置したラベルのBackStyleプロパティをいずれも「0 - 透明」にするだけの話である.
     しかし,元々あるラベルと違って,クリックしたときの動きが異なる.単にBackStyleプロパティを透明にしたラベルでは,デザイン時に配置した大きさだけがラベルとして認識され,文字がないところ,たとえば「0」という文字の真ん中をクリックしても,ラベルのClickイベントが発生する.
     ところが,透明なラベルActiveXコントロールとしたときには,ラベルの文字以外の部分をクリックしたときには,下にあるオブジェクト,たとえばフォーム自体にClickイベントが発生するのである.つまり,透明でスクロールするようなラベルコントロールをクリックしたときには,ActiveXコントロール側でClickイベントを知ることができないことがある,いや文字によってほとんど知ることができないのである.これを回避するためには,UserControlオブジェクト自体を透明にしない以外に手はない.


    不定形なActiveXコントロール
     透明なラベルコントロールでは,透明な部分はコントロールではなく,その下にあるオブジェクトであった.ということは,これはある意味で不定形なコントロールと考えることができる.
     これを押し進めてみると,UserControlオブジェクトにShapeコントロールなどの方形でないコントロールを配置し,BackStyleが透明なActiveXコントロールとすれば,不定形のActiveXコントロールとすることが可能である.
    次の例では,ShapeとLineコントロールを組み合わせて,うさぎの顔を作ってみた(図8).
     このActiveXコントロールをフォーム上に配置すれば,次のように不定形なコントロールを実現することができる.うさぎの耳の間をクリックすれば,フォームのClickイベントが発生する(図9).

    図8:うさぎの顔コントロール
    図9:フォームに配置

    一部が透明なピクチャー --さらに複雑な不定形コントロールの実現
     ActiveXコントロールの元であるUserDocumentオブジェクトには,見慣れないプロパティとして,MaskPictureとMaskColorがある.これは,マスクにする絵と,その絵の中の特定の色を透明色として設定するものである.
     次の例を見て欲しい.
    BackStyle = 0 -透明Pictureプロパティ   表示したい絵MaskPictureプロパティ	枠となる絵MaskColor	MaskPictureの枠の色

     以上のように指定すると,元絵を枠で囲んだような絵を作ることができる.
     なお,MaskPictureやMaskColorの設定は,BackStyleが透明のときのみ有効なので注意してほしい.

    図10:サンプルプログラム MaskPict

    Pictureプロパティの内容はそのまま表示されている
      
      
      
      MaskPictureプロパティには回りが白いこんな絵を入れておく



    MaskColorを白にしてフォームにコントロールを貼りつけると…

     ここで注意が必要なことがある.
     まず,Pictureプロパティに何か絵が入っていても,BackStyleを0-透明にすると,絵は出てこなくなる.これは,MaskPictureやMaskColorが有効になるからである.
     また,Picture, MaskPictureに同じ絵を読み込み,BackStyle = 0 -透明,MaskColorを適切に指定すると,元絵の特定の色が透過色になる.これもなかなか面白い.
     サンプルプログラムMaskPicでは,このあたりのプロパティの組み合わせによる違いがわかるようになっている.いろいろと試して動作を確認していただきたい(図11).

    図11:MaskPic

    コントロールのデザイン

    PictureとMaskPictureに同じ絵を指定し,MaskColorを絵にない色にすると,元絵がそのまま出てくる

    MaskColorをBlueにすると,Blueの部分だけが透明になり,フォームのBackColorになる.ここをクリックすれば,もちろん,フォームのClickイベントが発生する

    PictureプロパティをなくしMaskPictureだけにすれば,コントロールのBackColorである緑となり,そのうちMaskColorで指定された色だけが透明に抜ける

    アクセスキーに対応するには

     ボタン型のActiveXコントロールを作ることはカンタンだ.これには二つのアプローチがある.
     ひとつは既存のコマンドボタンをベースにすることだ.それではなんら意味がないと思われるかもしれない.Visual Basic 5.0のコマンドボタンではピクチャーを入れられたりと,Windows標準のボタンより高機能である.これをIEで使うようないわゆるインターネット用の部品と考えれば,それだけでも意味があるだろう.しかし,プログラミング的にはなんら面白くはない.
     もうひとつは,自分で独自の機能を持つようなボタンとしてコード主体でインプリメントすることだ.これはコード量が機能に比例して多くなるが,それなりに楽しいものを作ることができるだろう.
     さて,いずれの方法を使ってボタンを作ったときにも,アクセスキーの問題を避けて通ることはできない.アクセスキーとは,<Alt>+<キー>というキータイプでそのボタンが選ばれたことにする機能で,ふつうはコマンドボタンのCaptionプロパティなどに「&Add」と指定すると,表示は「Add」となり,「A」の下にアンダーラインがつく.<Alt>+<A>でこのボタンを押したことにするものだ.
     自分で作ったActiveXコントロールでアクセスキーに対応するには,AccessKeysプロパティにアクセスキーとなる文字を指定する.単に内在コントロールのコマンドボタンなどに「&Add」と入れておいても,アクセスキーにはならない.
     正確には,文字ではなく文字列である.つまり,“AD”と指定しておけば,ActiveXコントロール内にある二つのボタンに指定した「&Add」と「&Delete」の両方に対応することもできるということだ.
     ただし,他のコントロールのアクセスキーと競合するとうまく動かないので,競合しないようキャプションを変えられるようにするなどの注意が必要である.
     さらに,注意すべきことは,AccessKeyPressイベントと,内在コントロールのClickイベントの関係である.次にこの関係をまとめた.

    ・内在コントロールのアクセスキーに設定されていないアクセスキーが押されたときには,AccessKeyPressイベントが発生し,内在コントロールのClickイベントは発生しない
    ・内在コントロールのアクセスキーになっているときには,内在コントロールClickイベントが発生する

    ラベル型のコントロールにしたときのアクセスキーに対応するには

    図12:<Alt>+<W>とすれば幅を入力できる

     ラベルタイプのコントロールのときには,アクセスキーが押されたら次のタブインデックスのコントロールにフォーカスを移動するのがふつうだ.たとえば,「&Name」というラベルコントロールがあり,その次のタブインデックスにはテキストボックスがあり,画面上では並んでいるようなときだ.<Alt>+<N>とキータイプすれば,名前を入れるテキストボックスに移動するのである.これは,よく使われる方法だ.
     たとえば,Visual Basicのオプションダイアログなどでも,<Alt>+<W>とすれば幅を入力できるようになる(図12).もちろん,ラベルの後にあるのはテキストボックスとは限らず,リストボックスなどでもよい.


     単純なラベル型のActiveXコントロールを作っても,このような動作はしない.これを実現するためには,UserControlオブジェクトのForwardFocusプロパティをTrueにすればよい.

    デフォルトとキャンセルボタンに対応するには

     コマンドボタンには,CancelとDefaultのボタンがある.これらのプロパティをTrueにしておくと,テキストボックスなどにフォーカスがあるとき,それぞれ<Esc>キー,<Enter>キーを押すと,それらのボタンが押されたことになる.これはWindowsのユ−ザーインターフェイスデザインガイドで決まっていることである.
     よく,複数のテキストボックス間の移動を<Enter>キーでやりたいという話を聞くことがあるが,そんなことをしてしまうと,上記のことと整合性が取れなくなることは明らかだ.「労多ければバグも多し」というつまらないことになってしまうので,これはぜひやめてほしいことだ.
     話を戻そう.自分で描画するタイプのボタン型のActiveXコントロールを作り,このようなDefaultまたはCancelボタンにするには,UserControlオブジェクトのDefaultCancelプロパティをTrueにする必要がある.こうすれば,プロパティウィンドウにDefaultとCancelプロパティが出てくる.もちろん,これだけでDefaultとCancelの動作はするようになる.しかし,これだけでは足りないことがある.
     Defaultボタンとしたときには,他のボタン型コントロール以外,つまりテキストボックスなどにフォーカスがあったときには,Default = Trueとしたボタンの周りが黒く縁取られてDefaultボタンであると一目でわかるようになっていなければならないのだ.
     この描画は,次の二つのタイミングで行なう必要がある.

    1. AmbientChangedイベントでDisplayAsDefaultプロパティを監視する.Ambient.DisplayAsDefaultがTrueだったとき
    2. PaintまたはShowイベント

     実際には,次のようなコードを書けばよい(リスト8).

    リスト8:
    Private Sub UserControl_AmbientChanged(PropertyName As String)  If PropertyName = "DisplayAsDefault" Then    CheckDefault  End IfEnd SubPrivate Sub UserControl_Paint()  CheckDefaultEnd SubPrivate Sub CheckDefault()  If Ambient.DisplayAsDefault Then    UserControl.Line (0, 0)-(ScaleWidth - DrawWidth, ScaleHeight - DrawWidth), , B   Else     UserControl.Line (0, 0)-(ScaleWidth - DrawWidth, ScaleHeight - DrawWidth), BackColor, B  End IfEnd Sub

     また,フォーカスがあるときにはそれを示す点線枠を描くべきだ.これは,GotFocusおよびLostFocusイベントで描画すればよい.たとえば,次のようなコードで実現できる(リスト9).動作例を図13に示す.
     なお,当然だが,ラベル型に使うべきForwardFocursプロパティをTrueにしたときには,DefaultとCancelプロパティは使えなくなる.

    リスト9:
    Private Sub UserControl_GotFocus()  Dim dx As Single  Dim oldDrawStyle As Integer      dx = 3  oldDrawStyle = DrawStyle  DrawStyle = vbDot  Line (dx, dx)-(ScaleWidth - DrawWidth - dx, ScaleHeight - DrawWidth - dx), 0, B  DrawStyle = oldDrawStyleEnd SubPrivate Sub UserControl_LostFocus()  Picture = LoadPicture()End Sub

    図13:デフォルトとキャンセルボタンに対応

    ボタン型のActiveXコントロール


    テキストボックスにフォーカスがあるとき,一番下のActiveXコントロールボタンの周りには枠が描かれている

    コマンドボタン1にフォーカスがあるとき,一番下のコントロールの周りに枠はなく,コマンドボタン1にフォーカスがあることを示す点線と枠がある

    一番下のコントロールにフォーカスがあるとき,フォーカスがあることを示す点線と枠がある.

    AddressOf使用時にマルチインスタンスがおかしくなるのは

     以前にも紹介したAddressOfは,非常に強力な関数だ.これは,標準モジュールにある関数のアドレスを得るもので,Windows APIのEnumXXX系のコールバック関数を使ったり,サブクラス化などを行ないたいとき,非常に便利なものだ.
     しかし,AddressOfに使えるのは標準モジュールの関数である.つまり,これを使うときには,どうしても標準モジュールを併用する必要がある.
     ところが,ActiveXコントロールとして使ったとき,標準モジュールからUserControlメンバーを呼び出すことはできないのである.簡易的には,標準モジュールに用意したオブジェクト変数にユーザーコントロールオブジェクト自身であるMeをSetして呼び出すことができる.
     しかし,これはシングルインスタンスでのみ有効な手だ.というのも,ひとつのフォームに複数のActiveXコントロールを配置したときや,複数のフォームにActiveXコントロールを配置したとき,いずれもMeは新しいインスタンスであり,標準モジュールに宣言したオブジェクト型変数にもMeがコピーされてしまうからだ.これでは,最後に貼りつけられたインスタンスだけ正常に動作することになってしまう.マルチインスタンスで使うときには,インスタンスを区別する必要があるのだ.
     これを回避するには,次のような手法をとる必要がある.

    ・複数Form上のActiveXコントロールでのインスタンスの判断:フォームのTabプロパティにユニークなIDを入れるなどし,Formsコレクションと併用して判断する
    ・ひとつのForm上の複数のActiveXコントロールでのインスタンスの判断:配列あるいはクラスモジュールなどを使い,それらの対応をとって判断する

     実際にこれを解決するコードをいくつか書いてみたが,あまりスマートになっていないのが現状だ.しかし,この問題を認識していただきたいので,あえてここに書くことにした.
     もうしばらく時間をいただいて,スマートなコードを実現する方法を書かせていただくこととしたい.

    ActiveXコントロール作成は面白い

     今回のActiveXコントロール開発Tipsはいかがだっただろうか.Visual Basic 5.0が発売され,そこそこ時間も経ってきたこともあり,そこそこの話題を書くことができた.しかし,正直なところ,私自身あまり時間がなくていじり倒すところまで行っていない.いじっていけば,もっと面白いことがたくさんありそうだ(実は不定型サイズコントロールの実現や,実行時にドラッグして移動できるコントロールなど,まだまだ書きたいモノがあったのだが,誌面と時間の都合で今回は割愛した).
     読者諸氏も,「こんなことをしたいのだけど,どうしたらよいだろう」といった疑問などがあったら,PCデベロッパーネットワークのNetNews / Mailing Listに書いていただきたい.次回のTipsのネタとさせていただきたい.また,「こんなActiveXコントロールがあれば面白そうだ」というようなことも,どんどん書いていただきたい.
     しかし,Visual Basic 5.0はまだまだ不安定だ.Service Pack1をインストールしたが,相変わらずきちんと動かない部分や怪しいところが多い.信頼できる筋によると,近日中にService Pack2が出るという.Bug Fixのみならず機能もUpするらしいから,これは楽しみだ.もしかすると,この本が出る頃にはすでに配布が始まっているかもしれない.
     安定した環境があってこそ,初めて安心して開発もできるし,ノウハウも作っていける.バグ回避のノウハウばかりではつまらない.プログラミング自体をもっと楽しめる安定した環境になることを望みたい.

    本記事中に掲載したサンプル・プログラムは、ここからダウンロードできます。


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


    Copyright (c) 1997 int21 Corporation All Rights Reserved.
    For questions or comments, please send mail to: pcdn@int21.co.jp