Visual Basic 6.0 の優れた機能を取り戻す
Billy Hollis
May 14, 2003
概要: この資料では、Visual Basic .NET で、コントロール配列、Forms コレクション、およびフォーム上のすべてのコントロールを取得する Controls コレクションの作成方法について説明します。
サンプル ファイル (vbnet05132003_sample.exe) をダウンロードする。
Microsoft Visual Basic® 6.0 に関して物足りないことはそれほどありません。Visual Basic .NET は、私のソフトウェアの考え方にとても合っているように思えます。Visual Basic .NET には、Visual Basic 6.0 が提供する一部の機能に代わり、完全なオブジェクト指向機能を使用して、以前には行えなかった操作を行う柔軟性があります。優れた配置、Web インターフェイス、さらに巧妙な COM の詳細なども言うまでもありません。
ところが、開発者と話していて、Visual Basic 6.0 では利用できたのに、Visual Basic .NET では除外されたいくつかの機能を懐かしそうに話しているのを耳にしました。手馴れた方法で物事を確実に実現できるのに、その技法が使えなくなればがっかりするのはあたりまえです。
ただし、柔軟で完全なオブジェクト指向環境が優れている点は、通常、新しい技法を賢く使用すれば、このような古い技法を取り戻すことができることです。この資料では、Visual Basic .NET に含まれていない最も重要な次の 3 つの機能について説明します。
- コントロール配列
- Forms コレクション
- フォーム上のすべてのコントロールを取得する Controls コレクション
機能ごとに、Visual Basic .NET でその機能を再現する方法について説明します。
コントロール配列
Visual Basic 6.0 以前のコントロール配列は、フォーム上のすべて同じ種類のコントロールのグループです。これらは名前を共有します。通常のコントロールはその名前のみで識別できましたが、コントロール配列の 1 つのメンバへの参照を取得するには、名前とインデックスの両方が必要でした。
コントロール配列のすべてのメンバは、共通のイベントを共有していました。つまり、Visual Basic 6.0 では、配列内の任意のコントロールをクリックした場合、共通の Click イベント ハンドラにイベントが送信されます。イベントの引数の 1 つは、イベントを発生させた特定のコントロールを示すインデックスでした。
Visual Basic 6.0 以前で、開発者がコントロール配列を使用した主な理由は 3 つありました。
- 1 つのイベント ルーチンを複数のコントロールにフックできるようにするため。
- For Each ループで、コレクションとしてコントロールにアクセスするため。
- 実行中に新しいコントロールをフォームに追加するため。
このような操作はそれぞれ、Visual Basic .NET で簡単に実行できますが、この操作を行うための技法は Visual Basic 6.0 の開発者にとっては馴染みがありません。上記の各操作に対応する .NET の技法について説明しましょう。
コントロールをイベントにフックする
コントロール配列とイベントについて説明する前に、Visual Basic .NET のイベントを確認する必要があります。Visual Basic .NET では、イベントの動作が Visual Basic 6.0 とはかなり異なっています。最初は、その違いに気付かないことがあります。コード エディタでイベントを作成する方法は一見同じに見えます。
(WithEvents というキーワードを使用して宣言することにより) オブジェクトにイベントが含まれている場合、Visual Basic .NET のコード エディタの左側のドロップダウン メニューにそのオブジェクトが表示されます。そのリスト ボックスのオブジェクトを強調表示すると、そのオブジェクトのイベントが右側のドロップダウン メニューに表示されます。そのドロップダウン メニューでイベントを選択すると、イベント ルーチンの骨組みが得られます。
その骨組みルーチンのコードを確認すると、Visual Basic 6.0 と Visual Basic .NET のイベントの相違点のみがはっきりわかります。Visual Basic 6.0 では、コントロールまたはクラスのインスタンスのイベント ルーチンには標準名が付いています。標準名は、オブジェクト名、アンダースコア、およびイベント名で構成されています。そのため、txtAge という名前のテキスト ボックスの Click イベントは txtAge_Click になります。このコードは次のとおりです。
' VB6 バージョンのクリック イベント。
Private Sub txtAge_Click()
End Sub
Visual Basic .NET でも、txtAge_Click という名前の付いた txtAge の Click イベントを作成します。ただし、名前はただコードをより読みやすくするためだけのものです。実際、イベント ルーチンには好きな名前を付けることができます。
Visual Basic .NET によって最初に作成される、上記に相当する Click イベントは次のとおりです (コードを読みやすくするために少し変更しました)。
' VB.NET バージョンのクリック イベント。
Private Sub txtAge_Click(ByVal sender As Object, _
ByVal e As System.EventArgs) _
Handles txtAge.Click
End Sub
このイベント ルーチンの場合、この名前はルーチンをイベントにフックする名前ではありません。ルーチンの名前を txtAge_Click から MyFunkyEventRoutine に変更できますが、それほど影響はありません。
代わりに、ルーチンの宣言の最後に Handles 句があります。これが、ルーチンをイベントにフックします。この場合、フックされるイベントは、txtAge テキスト ボックスの Click イベントです。Handles txtAge.Click
が最後に示されているときは、ルーチンの名前に関係なく、テキスト ボックスをクリックすると、そのイベント ルーチンが実行されます。
他にも興味深いことがいくつかあります。まず、最後に Handles txtAge.Click
を記述した複数のルーチンを持つことができます。誰かが txtAge テキスト ボックスをクリックすると、Handles txtAge.Click
を含むすべてのルーチンが起動されます (余談ですが、イベント ルーチンが起動される順序は制御できないので、イベント ルーチンが特定の順番で起動されることに依存しないでください。これについては、この資料の後半でまた説明します)。
最後に、複数のコントロールを同じイベントに送信するために使用する技法を調べます。イベント ルーチンの最後の Handles 句には、カンマで区切って、好きな数だけイベントを記述できます。ここでは、3 つの異なるテキスト ボックスで生成された Click イベントで起動されるように、上記のイベント ルーチンを次のように少し変更します。
Private Sub MultiTextBox_Click(ByVal sender As Object, _
ByVal e As System.EventArgs) _
Handles txtAge.Click, txtHeight.Click, txtWeight.Click
End Sub
ルーチンの名前を txtAge_Click から MultiTextBox_Click に変更したことに注意してください。こうしておかないと、コードの読み手が txtAge 以外のテキスト ボックスでルーチンが動作することに気が付かないことがあるので、これは良い習慣です。
Visual Basic .NET でこの構文を使用すると、コントロール配列を必要としないで、同じイベントにコントロールをフックできます。この構文は、どのコントロールの Click イベントでも動作するので、柔軟性もあります。ユーザーは、イベント ルーチンが 1 種類のコントロールだけを処理するという制約を課せられません。さまざまな種類の一連のコントロールから Click イベントをトラップする共通のイベント ルーチンを含めることができます。chkHasCar および chkCertified という名前のチェック ボックスがある場合、上記のルーチンを変更して、テキスト ボックスとチェック ボックスからイベントを両方キャプチャできます。
Private Sub MultiControl_Click(ByVal sender As Object, _
ByVal e As System.EventArgs) _
Handles txtAge.Click, txtHeight.Click, txtWeight.Click, _
chkHasCar.Click, chkCertified.Click
End Sub
実際、イベントにまったく同じ引数リストがある限り、ルーチンは Click イベント以外に他のイベントもキャプチャできます。たとえば、Enter および DoubleClick イベントには、Click イベントと同じ引数リストがあります。上記のルーチンが次のようになると、1 つのテキスト ボックスおよび 1 つのチェック ボックスのこのようなすべてのイベントをキャプチャできます。
Private Sub CatchMultipleEvents(ByVal sender As Object, _
ByVal e As System.EventArgs) _
Handles txtAge.Click, txtAge.DoubleClick, _
txtAge.Enter, chkHasCar.Click, _
chkHasCar.DoubleClick, chkHasCar.Enter
End Sub
ご覧のとおり、コントロール配列が存在しなくてもその機能を失うことなく、実際には Visual Basic 6.0 よりもイベント処理に柔軟性があります。
For Each ループでコントロールにアクセスする
Visual Basic .NET にはコントロール配列が存在しないので、一連のコントロールをループすることが望ましい場合は、コントロールをコレクションに含める必要があります。この操作を最も簡単に行う方法として、コレクションを作成して、関連するコントロールを追加する方法があります。たとえば、CheckBox1、CheckBox2、および CheckBox3 という名前の 3 つのチェック ボックスを含むフォームがあると仮定します。次のように、フォームで ArrayList を宣言して、チェック ボックスをフォームに配置できます。
Dim colMyCheckBoxes As ArrayList
Private Sub BuildCheckBoxCollection()
colMyCheckBoxes = New ArrayList
colMyCheckBoxes.Add(CheckBox1)
colMyCheckBoxes.Add(CheckBox2)
colMyCheckBoxes.Add(CheckBox3)
End Sub
通常は、フォームの Load イベントでこのルーチンを実行します。このような Load イベントは、次のコードのようになります。
Private Sub Form1_Load(ByVal sender As Object, _
ByVal e As System.EventArgs) _
Handles MyBase.Load
BuildCheckBoxCollection()
End Sub
これで、すべてのチェック ボックスに影響する任意の操作で、チェック ボックスを簡単にループできるようになります。たとえば、フォーム上のボタンに次のようなコード ビハインドを配置することにより、オンになっているチェック ボックスの数が表示されます。
Dim obj As Object
Dim iCount As Integer = 0
For Each obj In colMyCheckBoxes
If TypeOf obj Is CheckBox Then
Dim chkCheckBox As CheckBox
chkCheckBox = CType(obj, CheckBox)
If chkCheckBox.Checked Then
iCount += 1
End If
End If
Next
MsgBox(iCount.ToString & " boxes checked")
ただし、コレクションの作成と管理の必要性をなくすショートカットがあります。パネル内にコントロールをまとめて配置すると、パネルの Controls プロパティを使用して、すべてのコントロールを保持するコレクションを自動的に取得できます。
実際の処理を見てみましょう。Panel1 という名前のパネルをフォームに追加して、3 つのチェック ボックスをパネルに移動すると仮定します。これで、colMyCheckBoxes コレクションを作成するコードを削除できるので、ボタンのコード ビハインドは次のようになります。
Dim ctl As Control
Dim iCount As Integer = 0
For Each ctl In Panel1.Controls
If TypeOf ctl Is CheckBox Then
Dim chkCheckBox As CheckBox
chkCheckBox = CType(ctl, CheckBox)
If chkCheckBox.Checked Then
iCount += 1
End If
End If
Next
MsgBox(iCount.ToString & " boxes checked")
この技法の 1 つの利点は、コレクションがコントロールを保持するだけであることです。つまり、一部の操作では、コレクション要素を特定の型に強制的にキャストする Ctype が必要ありません。たとえば、コレクション内のすべてのコントロールのフォントを太字に変える場合、コードは次のようになります。
Dim ctl As Control
For Each ctl In Panel1.Controls
Dim fnt As New Font(ctl.Font, FontStyle.Bold)
ctl.Font = fnt
Next
すべてのコントロールは Font プロパティを含むポリモーフィックなインターフェイスを共有するので、上記のコードでは、コレクション内のすべてのコントロールのフォントが太字になります。
両方の機能を統合する
複数のコントール間でイベントを共有する技法、およびコレクションにコントロールを配置する技法は、それぞれ単独でも適切に機能します。しかし、両方の技法を結び付けるものがないので、イベントの最初の技法では、新しいコントロールがイベントを共有する必要があるたびに、何らかのコーディングを行う必要があります。
ただし、Visual Basic .NET の高度なイベント機能を使用して、上記の 2 つの機能の結び付けることができます。またイベントを動的にフックする特別なバージョンのパネルも作成できます。このように、コントロール配列を制御するためにフォームにコードを記述しなくても、コントロール配列に近い機能を得ることができます。
inheritance を使用して、新しいパネルを新しい種類のコントロールとして作成します。Visual Basic .NET でのコントロールの作成については既に説明しました (「Creating Visual Basic .NET Controls From Scratch」 (英語) を参照してください)。そのため、ここではあまり詳しく説明しません。ここでは、イベントの管理方法の説明を追加して、順を追って手順を説明します。
パネル内に含まれるコントロールでイベントが発生するときに、そのイベントが共通ルーチンにまとめられ、パネルをホストするフォームで発生するように、イベントを構成します。この操作は驚くほど簡単です。手順は次のとおりです。
Visual Studio® .NET で新しい [Windows コントロール ライブラリ] プロジェクトを作成し、「ControlArrayPanel」という名前を付けます。
ソリューション エクスプローラで UserControl1.vb ファイルの名前を「ControlArrayPanel.vb」に変更します。
ControlArrayPanel.vb のコードに移動します。最初の数行は次のようになります。
Public Class UserControl1 Inherits System.Windows.Forms.UserControl
このコードを次のように変更します。
Public Class ControlArrayPanel Inherits System.Windows.Forms.Panel
このコードでは、新しい型のパネルにクラス名 (ControlArrayPanel) を指定して、その継承元の基本クラスが Windows Forms Panel コントロールであることを指定します。
イベントがすべてのコントロールの親である基本 Control 型のインターフェイスに含まれる限り、好きなだけさまざまな種類のイベントを処理するように、新しい ControlArrayPanel を変更できます。この例の場合、ControlArrayPanel がパネル内の各コントロールで以下のイベントを管理するようにします。
- Click
- KeyPress
- MouseUp
各イベントでは、ControlArrayPanel コントロールによって生成されたイベントをトラップして、統合します。統合したイベントは、ControlArrayPanel を含むフォームで発生することになります。つまり、ControlArrayPanel では、ControlArrayPanel を使用するフォームで発生するイベントが宣言されている必要があります。イベントは次のように宣言できます。
Public Event InternalControlClick(ByVal sender As Object, _ ByVal e As EventArgs) Public Event InternalControlKeyPress(ByVal sender As Object, _ ByVal e As KeyPressEventArgs) Public Event InternalControlMouseUp(ByVal sender As Object, _ ByVal e As MouseEventArgs)
このコードは、"
Windows フォーム デザイナで生成されたコード
" というラベルの付いたコードの領域のすぐ下に配置する必要があります。ControlArrayPanel のコントロールの各イベントをトラップするには、動的に接続するイベント ハンドラが必要です。トラップしようとするすべての型のイベントにこのようなイベント ハンドラの 1 つが必要です。手順 4 で記述した種類のイベントで機能する 3 つのイベント ハンドラのコードは次のとおりです。
Private Sub InternalClickHandler(ByVal sender As Object, _ ByVal e As EventArgs) RaiseEvent InternalControlClick(sender, e) End Sub Private Sub InternalKeyPressHandler(ByVal sender As Object, _ ByVal e As KeyPressEventArgs) RaiseEvent InternalControlKeyPress(sender, e) End Sub Private Sub InternalMouseUpHandler(ByVal sender As Object, _ ByVal e As MouseEventArgs) RaiseEvent internalControlMouseUp(sender, e) End Sub
このようなイベントのうち、ControlArrayPanel の "内部" のイベントは単純に対応する統合イベントを発生し、ControlArrayPanel の "外部" のイベントは、それを使用するフォームで発生します。上記のコードは、手順 4 のイベントの宣言のすぐ下に配置する必要があります。
ここで、コントロールがパネルに追加されるときに、これらのイベント ハンドラをフックする必要があります。この操作は、ControlArrayPanel の ControlAdded イベントで行う必要があります。ControlAdded イベントの完全なコードは次のとおりです。これは、手順 5 に追加したコードのすぐ下に記述できます。
Private Sub ControlArrayPanel_ControlAdded(ByVal sender As Object, _ ByVal e As System.Windows.Forms.ControlEventArgs) _ Handles MyBase.ControlAdded AddHandler e.Control.Click, AddressOf InternalClickHandler AddHandler e.Control.KeyPress, AddressOf InternalKeyPressHandler AddHandler e.Control.MouseUp, AddressOf InternalMouseUpHandler End Sub
AddHandler キーワードに馴染みのない方のために説明すると、このキーワードは、コントロールおよびコンポーネントで発生するイベントを、イベントを処理できる特定のサブルーチンに動的に接続します。このキーワードは、この資料で既に説明した実行時の Handles 句に代わるもので、特定のコントロールのイベントをルーチンにフックして、イベントを処理するときと同じ結果を実現します。
接続されるサブルーチンには、イベントを受け取るための適切な引数リストが必要になります。たとえば、InternalClickHandler ルーチンは、2 つの引数 (Object 型の sender、および EventArgs 型の e) を受け取ります。Click イベントの引数リストにはまったく同じ引数が含まれているので、Click イベントに接続できます (また、先に説明したとおり、DoubleClick および Enter イベントにも同じ引数リストがあるので、これらのイベントにも接続できます)。
AddHandler を使用して、e.Control.KeyPress を InternalClickHandler に接続しようとすると、"メソッド ... に、デリゲート ... と同じシグニチャがありません。" という構文エラーが表示されます。ここでは、イベントのメソッドの説明と引数リスト (場合によってはメソッドのシグニチャと言います) が省略符号に置き換えられています (エラー メッセージの詳細を確認してみてください)。
このロジックが ControlAdded イベントに配置されているので、ControlArrayPanel に追加されるすべてのコントロールで、Click イベント、KeyPress イベント、および MouseUp イベントがトラップされ、適切なイベントの統合ルーチンに渡されます。これまで説明してきたように、これらのルーチンはコントロールを使用しているフォームでイベントを発生させます。
ControlArrayPanel を強化するために、コントロールが ControlArrayPanel から削除されるときに、ハンドラを削除する必要があります。この操作を実行するには、次のコードのような ControlRemoved イベントを作成して、手順 6 で追加した ControlAdded イベントのすぐ下にコードを配置します。
Private Sub ControlArrayPanel_ControlRemoved(ByVal sender As Object, _ ByVal e As System.Windows.Forms.ControlEventArgs) _ Handles MyBase.ControlRemoved RemoveHandler e.Control.Click, AddressOf InternalClickHandler RemoveHandler e.Control.KeyPress, AddressOf InternalKeyPressHandler RemoveHandler e.Control.MouseUp, AddressOf InternalMouseUpHandler End Sub
RemoveHandler は、コントロールのイベントとそのイベントを処理している ControlArrayPanel の内部ルーチンとの間の接続を切断します。RemoveHandler の構文は AddHandler とまったく同じです。
これで、完全なコントロールになりました。コントロールをビルドして、間違いがないことを確認します。
コントロールをテストするには、[ファイル] メニューの [プロジェクトの追加] をポイントして、[新しいプロジェクト] をクリックします。[新しいプロジェクトの追加] ダイアログ ボックスで、[Windows アプリケーション] テンプレートを選択します。新しいプロジェクトに「TestControlArrayPanel」という名前を付けて、[OK] をクリックします。
ここで追加したテスト プロジェクトは、[デバッグ] メニューの [開始] をクリックするとき (またはツール バーの [開始] ボタンをクリックするとき) に開始されるプロジェクトにする必要があります。ソリューション (ソリューション エクスプローラの最初のエントリ) を右クリックして、[プロパティ] をクリックします。[プロパティ] ダイアログ ボックスで、[シングル スタートアップ プロジェクト] ボックスの [TestControlArrayPanel] をクリックし、[OK] をクリックします。
ドラッグ アンド ドロップ モードで [ControlArrayPanel] コントロールを使用するには、このコントロールをツールボックスに追加する必要があります。ツールボックスの [Window フォーム] タブを右クリックし、Visual Studio .NET 2002 では [ツールボックスのカスタマイズ]、Visual Studio .NET 2003 では [アイテムの追加と削除] をクリックします。ControlArrayPanel のプロジェクトを参照して、bin ディレクトリを参照します。ControlArrayPanel.dll という名前の DLL が含まれているので、その DLL を選択して、[開く] をクリックします。[OK] をクリックします。ControlArrayPanel のアイコンがツールボックスの下に表示されます。
TestControlArrayPanel で空の Form1 上に [ControlArrayPanel] コントロールをドラッグして、「ControlArrayPanel1」という名前にします。3 つのオプション ボタンを [ControlArrayPanel] にドラッグして、次のように名前とテキスト プロパティを設定します。
プロパティ名 設定するテキスト btnRed Red btnYellow Yellow btnLightGreen Light Green 1 つのボタンを [ControlArrayPanel] にドラッグします。その名前を「btnRestore」に設定して、テキスト プロパティを「Restore Color」に設定します。
Form1 のコード ウィンドウに移動します。コード エディタの左上のドロップダウン メニューで、[ControlArrayPanel1] をクリックします。その後、右側のドロップダウン メニューで、[InternalControlClick] イベントをクリックします。イベントの内部に、次のコードを配置します。
Dim ctlSender As Control ctlSender = CType(sender, Control) Select Case ctlSender.Name.ToUpper Case "optRed".ToUpper ControlArrayPanel1.BackColor = Color.Red Case "optYellow".ToUpper ControlArrayPanel1.BackColor = Color.Yellow Case "optLightGreen".ToUpper ControlArrayPanel1.BackColor = Color.LightGreen Case "btnRestore".ToUpper ControlArrayPanel1.BackColor = SystemColors.Control End Select
ControlArrayPanel でコントロールのいずれかをクリックすると、このイベントが発生します。引数
sender
は、ControlArrayPanel でクリックされたコントロールを識別します。そのコントロールが Object 型の場合、Control 型にキャストして、コントロールとしてアクセスする必要があります。このコードでは、最初の 2 行がその作業を行います。コントロールは、通常どおり、操作できます。最後に、フォームのデザイン ビューに戻り、ControlArrayPanel のボタンをダブルクリックして、ボタンの Click イベントを作成します。ボタンのイベントには、次のコードを配置します。
MsgBox("Restore color button pressed") Dim ctl As Control For Each ctl In ControlArrayPanel1.Controls If TypeOf (ctl) Is RadioButton Then Dim ctlRadioButton As RadioButton ctlRadioButton = CType(ctl, RadioButton) ctlRadioButton.Checked = False End If Next
このコードは、通常のパネルで可能であったように、ControlArrayPanel のコントロールをループできることを示します。そのため、イベントの統合およびコレクション内の複数のコントロールへのアクセスに関する 2 つの技法を ControlArrayPanel という 1 つのクラスに結合したことになります。
アプリケーションを実行してテストします。Yellow というラベルの付いたオプション ボタンをクリックすると、図 1 のような結果になります。ボタンをクリックすると、両方のイベント ルーチンがフォーム内で発生することに注意してください。
1 つのイベントを複数のハンドラにフックする場合、イベント ハンドラが特定の順序で発生することに依存しないでください。他のハンドラのロジックから完全に独立たロジックを、ハンドラごとに作成する必要があります。
図 1.ControlArrayPanel 内部の 3 つのオプション ボタンと 1 つのボタン。Yellow のオプション ボタンをクリックすると、オプションの Click イベントが Form1 の統合イベント ハンドラに送信されます。
実行中に新しいコントロールをフォームに追加する
Visual Basic 6.0 のフォームにコントロールを動的に追加するには、コントロールがコントロール配列に含まれている必要があります。これは重要な制限事項です。"シード" となるコントロールがフォーム上に既に存在し、コントロール配列を作成する必要があることを意味します。
Visual Basic 6.0 でフォーム上にコントロールを動的に配置することに関するすべての制限事項が、Windows フォームではなくなりました。ある程度まで、"すべて" のコントロールがフォーム上に動的に配置されます。これは、ビジュアル デザイナでコードを作成して、コントロールの作成およびプロパティの設定を行うためです。その構文を使用して、好きなときにコントロールをフォーム上に配置できます。
txtExtraStuff という名前のテキスト ボックスをフォーム上に配置するには、次のようなロジックを使用します。
Dim txtExtraStuff As New TextBox
txtExtraStuff.Top = 40
txtExtraStuff.Left = 180
txtExtraStuff.Text = "Some stuff"
txtExtraStuff.Width = 130
Me.Controls.Add(txtExtraStuff)
新しいクラスは他のクラスのようにインスタンスが作成されて、関連するプロパティを設定できます。設定しないプロパティは、既定値を取得します。
最後の行で、コントロールをフォーム上に表示します。フォームの Controls コレクションにコントロールを単純に追加すると、独自の画面を描画するときにフォームにコントロールが含まれます。
コントロールを追加してから、コントロールをイベント ハンドラにフックすることが望ましい場合があります。前の例で使用した AddHandler 構文は、この要求に対しても適切に対応します。トラップしようとする新しいコントロールのすべてのイベントに AddHandler 行を含めるだけです。言うまでもなく、前もってイベント ハンドラ ルーチンのコードを作成しておく必要があります。
実行中にコントロールを追加するためのこの技法は、とても柔軟で強力です。この技法を一度使用すると、今までコントロール配列を使用してフォームにコントロールを追加することがすばらしいと思っていたことが不思議に思われます。
Forms コレクションを取得する
Visual Basic 6.0 の開発者は、現在アプリケーションに読み込まれているフォームをループして、特定のフォームが読み込まれているかどうか、特定の種類のフォームがどれだけ読み込まれているかなど、何かを調べることがよくあります。Windows フォームには Forms コレクションがありませんが、Visual Basic .NET でこのような機能を得るには、追加の作業が必要になります。
この資料の前半でコントロールのコレクションを作成した方法と同様に、読み込まれたフォームを保持する独自のコレクションを実現するのは簡単です。問題は、アプリケーションのどこからでもコレクションにアクセスする方法、およびアプリケーション全体に一貫性のあるフォームを追加する方法です。
このような要求を両方とも解決する 1 つの優れた方法は、アプリケーション内のクラスの共有プロパティとしてコレクションを作成することです。また、そのクラスはフォームを表示するための共有メソッドを持つこともできるので、このメソッドにより、フォームが表示される前にそのフォームが既にコレクションに含まれることを保証できます。
このようなクラスを作成するには、Visual Basic .NET で新しい Windows アプリケーションを開始します。[プロジェクト] メニューの [クラスの追加] をクリックします。新しいクラスに「MyForms」という名前を付けます。新しいクラスのコードを次のコードに置き換えます。
Public Class MyForms
Private Shared m_colFormsCollection As New ArrayList
Public Shared Property Forms() As ArrayList
Get
Return m_colFormsCollection
End Get
Set(ByVal Value As ArrayList)
m_colFormsCollection = Value
End Set
End Property
Public Shared Sub Show(ByVal frm As Form)
Dim obj As Object
Dim bFound As Boolean = False
For Each obj In m_colFormsCollection
Dim frmCurrent As Form
frmCurrent = CType(obj, Form)
If frm Is frmCurrent Then
bFound = True
Exit For
End If
Next
If Not bFound Then
m_colFormsCollection.Add(frm)
End If
frm.Show()
End Sub
End Class
ここで、別のフォームを Windows アプリケーションに追加します。このフォームは、Form2 という名前を取得します。Form2 のデザイン画面に移動して、2 つのボタンをドラッグします。Button1 の Click イベントに、次のコードを配置します。
Dim f As New Form2
MyForms.Show(f)
Button2 の Click イベントに、次のコードを配置します。
Dim obj As Form
For Each obj In MyForms.Forms
Dim frm As Form = CType(obj, Form)
frm.Visible = False
Next
プログラムを実行してテストします。最初のボタンを数回クリックして、Form2 のインスタンスをいくつか取得します。2 番目のボタンをクリックすると、Form2 のすべてのインスタンスが表示されなくなります。
多くの Visual Basic 6.0 の開発者は、グローバル変数 (たとえば、Forms コレクションのグローバル変数) を使用して、この問題に対処しています。ただし、上記で使用したように、共有メンバを持つクラスの方が優れています。グローバル変数はプロジェクト内部のどこからでもそのまま使用できますが、さらに知的にコレクションを管理するためのロジックを持つこともできます。たとえば、上記の Show メソッドは、同じフォームをコレクションに追加しないことを保証します。
Controls コレクションとの相違点
Visual Basic 6.0 では、フォームの Controls コレクションは、フレームなどのフォーム上の別のコントロール内部に含まれているかどうかに関係なく、フォーム上のすべてのコントロールを返します。図 2 のように、フレームの内側に 2 つのボタン、外側に 1 つのボタンを持つフレームを含む Visual Basic 6.0 のフォームを作成することにより、この動作を確認できます。
図 2. Visual Basic 6.0 の Controls コレクションをテストするための Visual Basic 6.0 のフォーム
Visual Basic 6.0 で、Command3 の Click イベントに次のコードを配置します。
Dim ctl As Control
For Each ctl In Me.Controls
Debug.Print ctl.Caption
Next ctl
プログラムを実行して、[Command3] をクリックすると、デバッグ ウィンドウに以下の 4 行が表示されます。
Command3
Frame1
Command2
Command1
Visual Basic .NET で同様の操作を行うと、別の結果が表示されます。Visual Basic .NET で、1 つのグループボックスと 3 つのボタンを含む同様のフォームを図 3 のように作成します。
図 3. Visual Basic .NET で Controls コレクションをテストするフォーム
Button3 の Click イベントに次のロジックを配置します。
Dim ctl As Control
For Each ctl In Me.Controls
Console.WriteLine(ctl.Text)
Next
このプログラムを実行して、[Button3] をクリックすると、出力ウィンドウに 2 行の出力が表示されます。
Button3
GroupBox1
グループボックス内の 2 つのボタンは Form1 の Controls コレクションに含まれていないので、この出力は表示されません。ただし、これらのボタンは GroupBox1 の Controls コレクションに含まれています。
Windows フォームでは、任意のレベルで、他のコントロール内部に順次コントロールを含めることができます。では、フォーム上のすべてのコントロールを取得するにはどうすればよいのでしょうか?
これは、再帰を必要とする典型的な例です。最後のレベルに到達し、すべてのレベルで検出されたすべてのコントロールを収集するまで、いくつものコンテナ レベルを再帰する関数を作成できます。
Visual Basic .NET の上記のフォームに次の 2 つの関数を追加します。
Private Function AllControls(ByVal frm As Form) As ArrayList
Dim colControls As New ArrayList
AddContainerControls(frm, colControls)
Return colControls
End Function
Private Sub AddContainerControls(ByVal ctlContainer As Control, _
ByVal colControls As ArrayList)
Dim ctl As Control
For Each ctl In ctlContainer.Controls
colControls.Add(ctl)
AddContainerControls(ctl, colControls)
Next
End Sub
最初の関数は、すべてのコントロールの保持に使用する ArrayList をセットアップし、2 番目の関数は、各コントロールに順番に他のコントロールが含まれているかどうか再帰的に確認しながらコントロールを追加します。
ここで、「Button4」という名前の新しいボタンをフォームに追加して、そのコードに次のロジックを配置します。
Dim ctl As Control
For Each ctl In AllControls(Me)
Console.WriteLine(ctl.Text)
Next
フォームを実行して、ボタンをクリックすると、ボタンの位置に関係なく、フォーム上のすべてのコントロールに出力されます。たとえば、グループボックス内部のパネルやパネル内部のいくつかのチェック ボックスなど、もう少し多くのコントロールをフォーム上にドラッグすると仮定します。最終的に、図 4 のようになります。
図 4. Visual Basic .NET で新しい AllControls コレクションをテストするフォーム
ここで、プログラムを実行して [Button4] をクリックすると、出力テキストは次のようになります。
Button4
Button3
GroupBox1
CheckBox2
CheckBox1
Button2
Button1
空白行に注意してください。これはグループボックスに含まれるパネルの行です。既定では、パネル コントロールには空のテキスト プロパティがあります。
まとめ
Visual Basic 6.0 から Visual Basic .NET への移行に恐れを抱くかもしれません。Visual Basic .NET は、完全なオブジェクト指向の開発環境として、コントロール配列など、オブジェクト機能のギャップを埋めている Visual Basic 6.0 の機能の一部を除外しています。せっかく習得した機能がサポートされないことにがっかりする場合もありますが、ここまで説明してきたように、ほとんどの場合にその機能はまだ存在しています。その機能に、さまざまな方法でアクセスする必要があるだけです。Visual Basic .NET の新機能は、より多くの柔軟性と能力を大幅に増加し、移行を実に役立つものにしています。
Billy Hollis は、Visual Basic .NET に関する初めての著作『VB.NET Programming with the Public Beta』を (Rocky Lhotka と) 共同執筆し、主な産業会議で定期講演を行っています。彼は、マイクロソフトの 2001 年の MSDN Regional Director でしたが、現在は、マイクロソフトの INETA の講演者の事務局のメンバです。彼は、市販用ソフトウェアおよびスマート クライアントの開発を専門とする、独自の .NET コンサルティング業務を行っています。また、全国各地で Visual Basic.NET のトレーニングも行っています。