この記事では、TreeView コントロールの使用方法に焦点を当てます。 TreeView コントロールは豊富な機能を備えており、さまざまな状況で使用できます。概要: データ バインディング機能を TreeView コントロールに追加する方法について説明します。これは、一連の Microsoft Windows コントロール開発例の 1 つです。この記事は、関連する概要記事と合わせて読むことができます。
導入
可能であれば、既製のコントロールから始める必要があります。提供されている Microsoft® Windows® Forms コントロールには非常に多くのコーディングとテストが含まれているため、それらを放棄して最初から始めるのはもったいないからです。これを踏まえて、この例では既存の Windows フォーム コントロール TreeView を継承してカスタマイズします。 TreeViewコントロールのコードをダウンロードすると、追加のコントロール開発例と、拡張されたTreeView を他のデータ バインド コントロールで使用する方法を示すサンプル アプリケーションも入手できます。
データ バインディング ツリー ビューの設計
Windows 開発者にとって、 TreeViewコントロールへのデータ バインディングの追加は一般的な問題ですが、 TreeViewと他のコントロール ( ListBoxやDataGridなど) の間には 1 つの大きな違いがある (つまり、 TreeView は階層データを表示します) ため、基本的なコントロールのこの機能は次のとおりです。まだサポートされていません (つまり、まだ使用する必要があります)。データ テーブルがあれば、その情報をListBoxまたはDataGridに表示する方法は明らかですが、 TreeViewの階層化された性質を利用して同じデータを表示するのはそれほど簡単ではありません。私は個人的に、 TreeView を使用してデータを表示するときにさまざまな方法を適用してきましたが、最も一般的に使用されている方法が 1 つあります。それは、図 1 に示すように、テーブル内のデータを特定のフィールドごとにグループ化することです。
図 1: TreeView でのデータの表示
この例では、フラット データセット (図 2 を参照) を渡して、図 1 に示す結果を簡単に生成できるTreeViewコントロールを作成します。
図 2: 図 1 に示すツリーの作成に必要なすべての情報を含むフラットな結果セット
コーディングを開始する前に、この特定のデータ セットを処理する新しいコントロールの設計を思いつきました。他の多くの同様の状況でも機能することを願っています。ほとんどのフラット データを使用して階層構造を作成するのに十分な大きさのグループ コレクションを追加します。この場合、階層レベルごとにグループ化フィールド、表示フィールド、および値フィールドを指定します (いずれかまたはすべてのフィールドが同じである必要があります)。図 2 に示すデータを図 1 に示すTreeViewに変換するには、新しいコントロールでは、発行者とタイトルという 2 つのグループ化レベルを定義し、発行者グループのグループ化フィールドとしてpub_id を定義し、グループ化フィールドとしてtitle_id を定義する必要があります。タイトルグループのフィールド。グループ化フィールドに加えて、各グループの表示フィールドと値フィールドを指定して、対応するグループ ノードに表示されるテキストと、特定のグループを一意に識別する値を決定する必要もあります。この種のデータが見つかった場合は、 pub_name/pub_idおよびtitle/title_id をこれら 2 つのグループの表示/値フィールドとして使用します。著者情報はツリーのリーフ ノード (グループ化階層の最後のノード) になり、これらのノードの ID ( au_id ) フィールドと表示 ( au_lname ) フィールドも指定する必要があります。
カスタム コントロールを構築する場合、コーディングを開始する前にプログラマがコントロールをどのように使用するかを決定すると、コントロールをより効率的にすることができます。この場合、プログラマが (前に示したデータと望ましい結果を考慮して) 次のような数行のコードでグループ化を完了できるようにしたいと考えています。
DbTreeControl を使用する場合 .ValueMember = au_id .DisplayMember = au_lname .DataSource = myDataTable.DefaultView .AddGroup(発行者、pub_id、pub_name、pub_id) .AddGroup(タイトル, タイトル ID, タイトル, タイトル ID) で終わる |
注:これは私が書いたコードの最終行ではありませんが、似ています。コントロールの開発中に、 TreeViewに関連付けられたImageList内のイメージ インデックスを各グループ化レベルに関連付ける必要があることに気づき、 AddGroupメソッドに追加のパラメーターを追加する必要がありました。
実際にツリーを構築するには、データを調べて、必要に応じて新しいグループ化ノードと各データ項目のリーフ ノードを作成しながら、フィールド (各グループ化のグループ化値として指定) への変更を探します。ノードをグループ化しているため、ノードの合計数はデータ ソース内の項目の数よりも多くなりますが、基になるデータ内の項目ごとにリーフ ノードが 1 つだけ存在します。
図 3: グループ ノードとリーフ ノード
リーフ ノードとグループ ノードの区別 (図 3 を参照) は、この記事の残りの部分で重要になります。これら 2 つのタイプのノードを別々に扱い、ノードのタイプごとにカスタム ノードを作成し、選択したノード タイプに基づいて異なるイベントを発生させることにしました。
データバインディングを実装する
このコントロールのコードを記述する最初のステップは、プロジェクトと対応する開始クラスを作成することです。この例では、まず新しい Windows コントロール ライブラリを作成し、次にデフォルトのUserControlクラスを削除して、 TreeViewコントロールから継承する新しいクラスに置き換えます。
パブリック クラス dbTreeControl
System.Windows.Forms.TreeView を継承します
ここからは、フォーム上に配置でき、通常のTreeViewの外観と機能を持つコントロールを設計します。次のステップでは、 TreeViewに追加される新しい機能、つまりデータ バインディングとデータのグループ化を処理するために必要なコードの追加を開始します。
DataSource プロパティを追加する
新しいコントロールの機能はすべて重要ですが、複雑なデータ バインド コントロールを構築する際の 2 つの重要な問題は、 DataSourceプロパティを処理することと、データ ソース内の各オブジェクトから個々の項目を取得することです。
属性ルーチンの作成
まず、複雑なデータ バインディングの実装に使用されるコントロールは、 DataSourceプロパティ ルーチンを実装し、適切なメンバー変数を維持する必要があります。
オブジェクトとしてのプライベート m_DataSource _ オブジェクトとしてのパブリック プロパティ DataSource() 得る m_DataSource を返す 終了取得 Set(ByVal 値をオブジェクトとして) 価値が何もない場合 cm = なし GroupingChanged() それ以外 そうでない場合 (値のタイプが IList または _ TypeOf Value は IListSource) ' はこの目的に有効なデータ ソースではありません 新しい System.Exception (無効なデータソース) をスローします それ以外 TypeOf Value が IListSource の場合 myListSource を IListSource として暗くする myListSource = CType(値, IListSource) myListSource.ContainsListCollection = True の場合 新しい System.Exception (無効なデータソース) をスローします それ以外 「はい、はい。それは有効なデータソースです m_DataSource = 値 cm = CType(Me.BindingContext(Value), _ 通貨マネージャー) GroupingChanged() 終了の場合 それ以外 m_DataSource = 値 cm = CType(Me.BindingContext(Value), _ 通貨マネージャー) GroupingChanged() 終了の場合 終了の場合 終了の場合 エンドセット 終了プロパティ |
複雑なデータ バインディングのデータ ソースとして使用できるオブジェクトは通常サポートされており、このインターフェイスはデータをオブジェクトのコレクションとして公開し、 Countなどのいくつかの便利なプロパティを提供します。新しいTreeViewコントロールでは、バインディングにIListを使用したオブジェクトが必要ですが、別のインターフェイスを使用すると、 IListオブジェクトを取得する便利な方法 ( GetList ) が提供されるため、問題なく動作します。 DataSourceプロパティを設定するときは、まず有効なオブジェクト、つまりIListまたはIListSource をサポートするオブジェクトが提供されているかどうかを確認します。私が本当に欲しいのはIListなので、オブジェクトがIListSource (例: DataTable ) のみをサポートしている場合は、そのインターフェイスのGetList()メソッドを使用して正しいオブジェクトを取得します。
IListSource を実装する一部のオブジェクト ( DataSetなど) には、実際にはContainsListCollectionプロパティで表される複数のリストが含まれています。このプロパティがTrueの場合、 GetList はリスト (複数のリストを含む) を表すIListオブジェクトを返します。この例では、 IListオブジェクト、またはIListオブジェクトを 1 つだけ含むIListSourceオブジェクトへの直接接続をサポートし、 DataSetなどのデータ ソースを指定するために追加の作業が必要なオブジェクトを無視することにしました。
注:このようなオブジェクト ( DataSetなど) をサポートする場合は、追加のプロパティ ( DataMemberなど) を追加して、バインドする特定のサブリストを指定できます。
指定されたデータ ソースが有効な場合、最終結果はインスタンスが作成されます ( cm = Me.BindingContext(Value) )。このインスタンスは、基になるデータ ソース、オブジェクト プロパティ、および位置情報にアクセスするために使用されるため、ローカル変数に格納されます。
表示および値のメンバー プロパティを追加する
DataSource を作成することは、複雑なデータ バインディングを実装するための最初のステップですが、コントロールは、データのどの特定のフィールドまたはプロパティが表示メンバーおよび値メンバーとして使用されるかを認識する必要があります。 Displayメンバーはツリー ノードのタイトルとして使用され、 ValueメンバーはノードのValueプロパティを通じてアクセスできます。これらのプロパティはすべてフィールド名またはプロパティ名を表す文字列であり、コントロールに簡単に追加できます。
文字列としてのプライベート m_ValueMember 文字列としてのプライベート m_DisplayMember _ Public プロパティ ValueMember() としての文字列 得る m_ValueMember を返す 終了取得 Set(ByVal 値を文字列として) m_ValueMember = 値 エンドセット 終了プロパティ _ Public プロパティ DisplayMember() As String 得る m_DisplayMember を返す 終了取得 Set(ByVal 値を文字列として) m_DisplayMember = 値 エンドセット 終了プロパティ |
このTreeViewでは、これらのプロパティはリーフ ノードのDisplayメンバーとValueメンバーのみを表し、各グループ化レベルの対応する情報はAddGroupメソッドで指定されます。
CurrencyManager オブジェクトの使用
前に説明したDataSourceプロパティでは、 CurrencyManagerクラスのインスタンスが作成され、クラス レベルの変数に格納されます。このオブジェクトを通じてアクセスされるCurrencyManagerクラスは、次の機能を有効にするプロパティ、メソッド、イベントを備えているため、データ バインディングの実装の重要な部分です。
属性/フィールド値の取得
CurrencyManagerオブジェクトを使用すると、 GetItemPropertiesメソッドを通じてデータ ソース内の個々のアイテムからプロパティまたはフィールド値 ( DisplayMember フィールドやValueMemberフィールドの値など) を取得できます。次に、 PropertyDescriptorオブジェクトを使用して、特定のリスト項目の特定のフィールドまたはプロパティの値を取得します。次のコード スニペットは、これらのPropertyDescriptorオブジェクトを作成する方法と、 GetValue関数を使用して基になるデータ ソース内の項目のプロパティ値を取得する方法を示しています。 CurrencyManagerオブジェクトのListプロパティに注目してください。これにより、コントロールがバインドされているIListインスタンスへのアクセスが提供されます。
私のNewLeafNodeをTreeLeafNodeとして暗くする オブジェクトとしての Dim currObject currObject = cm.List(currentListIndex) Me.DisplayMember <> AndAlso Me.ValueMember <> の場合 'リーフノードを追加しますか? Dim pdValue As System.ComponentModel.PropertyDescriptor Dim pdDisplay As System.ComponentModel.PropertyDescriptor pdValue = cm.GetItemProperties()(Me.ValueMember) pdDisplay = cm.GetItemProperties()(Me.DisplayMember) myNewLeafNode = _ New TreeLeafNode(CStr(pdDisplay.GetValue(currObject)), _ currオブジェクト、_ pdValue.GetValue(currObject), _ 現在のリストインデックス) |
GetValue はオブジェクトを返すときにプロパティの基礎となるデータ型を無視するため、戻り値を使用する前に変換する必要があります。
データ バインドされたコントロールの同期を維持する
CurrencyManager にはもう 1 つの主要な機能があります。バインドされたデータ ソースと項目プロパティへのアクセスを提供することに加えて、同じDataSource を使用してこのコントロールと他のコントロール間のデータ バインディングを調整できるようになります。このサポートを使用すると、同じデータ ソースに同時にバインドされている複数のコントロールがデータ ソースの同じ項目に留まるようにすることができます。私のコントロールでは、ツリー内で項目が選択されたときに、同じデータ ソースにバインドされている他のすべてのコントロールが同じ項目 (同じレコード、行、または必要に応じて配列) を指していることを確認したいと考えています。データベースの観点から考えること)。これを行うには、ベースTreeViewのOnAfterSelectメソッドをオーバーライドします。そのメソッド (ツリー ノードの選択後に呼び出されます) では、 CurrencyManagerオブジェクトのPositionプロパティを現在選択されている項目のインデックスに設定します。 TreeViewコントロールで提供されるサンプル アプリケーションは、同期コントロールによってデータ バインドされたユーザー インターフェイスの構築がどのように簡単になるかを示しています。現在選択されている項目のリスト位置を簡単に判断できるように、カスタムTreeNodeクラス ( TreeLeafNodeまたはTreeGroupNode ) を使用し、作成したPositionプロパティに各ノードのリスト インデックスを保存しました。
保護されたオーバーライド サブ OnAfterSelect _ (System.Windows.Forms.TreeViewEventArgs としての ByVal e) TreeLeafNode としての Dim tln TypeOf e.Node が TreeGroupNode の場合 tln = FindFirstLeafNode(e.Node) Dim groupArgs As New groupTreeViewEventArgs(e) RaiseEvent AfterGroupSelect(groupArgs) ElseIf TypeOf e.Node が TreeLeafNode の場合 新しいleafTreeViewEventArgs(e)として薄暗いleafArgs RaiseEvent AfterLeafSelect(leafArgs) tln = CType(e.Node, TreeLeafNode) 終了の場合 そうでない場合、tln は何もありません cm.位置 <> tln.位置の場合 cm.位置 = tln.位置 終了の場合 終了の場合 MyBase.OnAfterSelect(e) エンドサブ |
前のコード スニペットで、 FindFirstLeafNodeという関数に気づいたかもしれませんが、ここで簡単に紹介したいと思います。私のTreeViewでは、リーフ ノード (階層の最後のノード) のみがDataSource内の項目に対応し、他のすべてのノードはグループ化構造を作成するためにのみ使用されます。パフォーマンスの良いデータ バインド コントロールを作成したい場合は、 DataSourceに対応する項目を常に選択する必要があるため、グループ ノードを選択するたびに、このノードが現在の選択範囲。実際のサンプルを確認することができますが、今のところは自由に使用できます。
プライベート関数 FindFirstLeafNode(ByVal currNode As TreeNode) _ TreeLeafNodeとして TypeOf currNode が TreeLeafNode の場合 CType(currNode, TreeLeafNode) を返す それ以外 currNode.Nodes.Count > 0 の場合 FindFirstLeafNode(currNode.Nodes(0)) を返す それ以外 何も返さない 終了の場合 終了の場合 終了機能 |
CurrencyManagerオブジェクトのPositionプロパティを設定すると、他のコントロールが現在の選択と同期しますが、他のコントロールの位置が変更されると、 CurrencyManagerもイベントを生成して、それに応じて選択が変更されます。適切なデータ バインド コンポーネントにするためには、データ ソースの場所が変更されると選択範囲が移動し、項目のデータが変更されると表示が更新される必要があります。 CurrencyManagerによって発生するイベントは 3 つあります: CurrentChanged 、 ItemChanged 、 PositionChangedです。最後のイベントは非常に単純です。CurrencyManager の目的の 1 つは、複数のバインドされたコントロールがすべて同じレコードまたはリスト項目を表示できるように、データ ソースの現在の位置インジケーターを維持することであり、位置が変更されるたびにこのイベントが発生します。他の 2 つのイベントは重複する場合があり、区別が明確ではありません。以下では、カスタム コントロールでこれらのイベントを使用する方法について説明します。 PositionChangedは比較的単純なイベントなので、複雑なデータ バインド コントロール (Tree など) で現在選択されている項目を調整する場合は説明しません。イベント。 ItemChangedイベントはデータ ソース内の項目が変更されるたびに発生しますが、 CurrentChangedは現在の項目が変更された場合にのみ発生します。
TreeViewでは、新しい項目を選択するたびに 3 つのイベントがすべて発生することがわかりました。そのため、現在選択されている項目を変更してPositionChangedイベントを処理し、他の 2 つのイベントには何もしないことにしました。データ ソースをIBindingListにキャストし (データ ソースがIBindingList をサポートしている場合)、代わりにListChangedイベントを使用することをお勧めしますが、私はこの機能を実装していません。
Private Sub cm_PositionChanged(ByVal sender As Object, _ ByVal e As System.EventArgs) cm.PositionChanged を処理します TreeLeafNode としての Dim tln TypeOf Me.SelectedNode が TreeLeafNode の場合 tln = CType(Me.SelectedNode, TreeLeafNode) それ以外 tln = FindFirstLeafNode(Me.SelectedNode) 終了の場合 If tln.Position <> cm.Position then Me.SelectedNode = FindNodeByPosition(cm.Position) 終了の場合 エンドサブ プライベート オーバーロード関数 FindNodeByPosition(ByVal インデックス As Integer) _ ツリーノードとして FindNodeByPosition(インデックス, Me.Nodes) を返す 終了機能 プライベート オーバーロード関数 FindNodeByPosition(ByVal インデックス As Integer, _ ByVal NodesToSearch As TreeNodeCollection) As TreeNode Dim i As Integer = 0 TreeNode としてのディム currNode TreeLeafNode としての Dim tln Do While i < NodesToSearch.Count currNode = NodesToSearch(i) 私 += 1 TypeOf currNode が TreeLeafNode の場合 tln = CType(currNode, TreeLeafNode) tln.Position = インデックスの場合 戻り値 currNode 終了の場合 それ以外 currNode = FindNodeByPosition(インデックス, currNode.Nodes) currNode が何もない場合 戻り値 currNode 終了の場合 終了の場合 ループ 何も返さない 終了機能 |
データソースをツリーに変換する
データ バインディング コードを作成した後、グループ化レベルを管理するコードを追加し、それに応じてツリーを構築し、いくつかのカスタム イベント、メソッド、およびプロパティを追加します。
管理グループ
グループのコレクションを構成するには、プログラマはAddGroup 、 RemoveGroup 、およびClearGroups関数を作成する必要があります。グループ コレクションが変更されるたびに、(新しい構成を反映するために) ツリーを再描画する必要があるため、状況が変化してツリーを強制的に描画する必要がある場合に、コントロール内のさまざまなコードによって呼び出すことができる汎用プロシージャGroupingChanged を作成しました。再構築される:
新しい ArrayList() としてのプライベート TreeGroups Public Sub RemoveGroup(ByVal グループ As Group) TreeGroups.Contains(group) ではない場合 TreeGroups.Remove(グループ) GroupingChanged() 終了の場合 エンドサブ パブリック オーバーロード Sub AddGroup(ByVal グループ As Group) 試す TreeGroups.Add(グループ) GroupingChanged() キャッチ 試行を終了する エンドサブ パブリック オーバーロード Sub AddGroup(ByVal name As String, _ ByVal groupBy As String、_ ByVal 文字列としての表示メンバー、_ ByVal valueMember As String、_ ByVal imageIndex を整数として、_ ByVal selectedImageIndex As Integer) myNewGroup を新しいグループとして薄暗くする(name, groupBy, _ 表示メンバー、値メンバー、_ imageIndex、selectedImageIndex) Me.AddGroup(myNewGroup) エンドサブ Group() としてのパブリック関数 GetGroups() 戻り値 CType(treeGroups.ToArray(GetType(Group)), Group()) 終了機能 |
スパニングツリー
ツリーの実際の再構築は、 BuildTreeとAddNodes という1 組のプロセスによって行われます。これら 2 つのプロセスのコードは長すぎるため、この記事ではすべてをリストするのではなく、それらの動作を要約します (もちろん、必要に応じて完全なコードをダウンロードすることもできます)。前述したように、プログラマは一連のグループを設定し、 BuildTreeでこれらのグループを使用してツリー ノードの設定方法を決定することで、このコントロールを操作できます。 BuildTree は現在のノード コレクションをクリアし、データ ソース全体を反復して第 1 レベルのグループ化 (この記事の前半の例と図で説明したパブリッシャー) を処理し、異なるグループ化値ごとにノードを追加します (例のデータを使用し、それぞれにpub_id値を持つノードを追加し、 AddNodesを呼び出して、第 1 レベルのグループの下にすべてのノードを設定します。 AddNodes は、それ自体を再帰的に呼び出して、必要な数のレベルを処理し、必要に応じてグループ ノードとリーフ ノードを追加します。 TreeNodeに基づく 2 つのカスタム クラスを使用してグループ ノードとリーフ ノードを区別し、2 種類のノードに対応する属性を提供します。
カスタム ツリービュー イベント
ノードが選択されるたびに、 TreeView はBeforeSelectとAfterSelectという 2 つのイベントを発生させます。しかし、私のコントロールでは、グループ ノードとリーフ ノードのイベントを異なるものにしたかったので、基本イベントに加えて、独自のイベントBeforeGroupSelect/AfterGroupSelectおよびBeforeLeafSelect/AfterLeafSelectを追加しました。
パブリック イベント BeforeGroupSelect _ (オブジェクトとしての ByVal 送信者、groupTreeViewCancelEventArgs としての ByVal e) パブリック イベント AfterGroupSelect _ (オブジェクトとしての ByVal 送信者、groupTreeViewEventArgs としての ByVal e) パブリック EventBeforeLeafSelect _ (オブジェクトとしての ByVal 送信者、leafTreeViewCancelEventArgs としての ByVal e) 公開イベント AfterLeafSelect _ (オブジェクトとしての ByVal 送信者、leafTreeViewEventArgs としての ByVal e) 保護されたオーバーライド サブ OnBeforeSelect _ (System.Windows.Forms.TreeViewCancelEventArgs としての ByVal e) TypeOf e.Node が TreeGroupNode の場合 Dim groupArgs As New groupTreeViewCancelEventArgs(e) RaiseEvent BeforeGroupSelect(CObj(Me), groupArgs) ElseIf TypeOf e.Node が TreeLeafNode の場合 DimリーフArgsを新しいleafTreeViewCancelEventArgs(e)として RaiseEvent BeforeLeafSelect(CObj(Me), LeafArgs) 終了の場合 MyBase.OnBeforeSelect(e) エンドサブ 保護されたオーバーライド サブ OnAfterSelect _ (System.Windows.Forms.TreeViewEventArgs としての ByVal e) TreeLeafNode としての Dim tln TypeOf e.Node が TreeGroupNode の場合 tln = FindFirstLeafNode(e.Node) Dim groupArgs As New groupTreeViewEventArgs(e) RaiseEvent AfterGroupSelect(CObj(Me), groupArgs) ElseIf TypeOf e.Node が TreeLeafNode の場合 新しいleafTreeViewEventArgs(e)として薄暗いleafArgs RaiseEvent AfterLeafSelect(CObj(Me)、leafArgs) tln = CType(e.Node, TreeLeafNode) 終了の場合 そうでない場合、tln は何もありません cm.位置 <> tln.位置の場合 cm.位置 = tln.位置 終了の場合 終了の場合 MyBase.OnAfterSelect(e) エンドサブ |
カスタム ノード クラス ( TreeLeafNodeおよびTreeGroupNode ) とカスタム イベント パラメーター クラスは、ダウンロード可能なコードに含まれています。
サンプルアプリケーション
このサンプル コントロールのすべてのコードを完全に理解するには、それがアプリケーションでどのように機能するかを理解する必要があります。付属のサンプル アプリケーションは、pubs.mdb Access データベースを使用し、ツリーコントロールを他のデータ バインド コントロールと組み合わせて Windows アプリケーションを作成する方法を示します。この場合に特に注目すべき主な機能には、ツリーと他のバインドされたコントロールとの同期、およびデータ ソースで検索を実行するときのツリー ノードの自動選択が含まれます。
注:このサンプル アプリケーション (TheSample という名前) は、この記事のダウンロードに含まれています。
図 4: データ バインドされた TreeView のデモ アプリケーション
まとめ
この記事で説明するデータ バインドツリーコントロールは、データベース情報を表示するためにツリーコントロールを必要とするすべてのプロジェクトに適しているわけではありませんが、個人的な目的に合わせてコントロールをカスタマイズする方法が導入されています。構築する複雑なデータ バインド コントロールには、ツリー コントロールとほぼ同じコードが含まれており、既存のコードを変更することで将来のコントロール開発を簡素化できることに注意してください。