2010年12月15日水曜日

(C#, VB.NET)複数列コンボボックス


Windows Forms のコンボボックスは複数列に対応しておりません。Access(アクセス)の様な複数列コンボボックスの動きをさせるには DrawItem イベント内で自前の描画(オーナードロー)を行います。

以下は、簡単なサンプルになります。

まず、標準的なコンボボックスを作成していきます。

データ表示用のデータテーブルを用意します。各列の DataType は初期値の System.String のままで製品ID(ProductID)と製品名(ProductName)の2列を作成します。製品IDは6桁固定で null は無しとします。

フォームに用意したデータセットを配置します。



サンプルなのでフォームロードイベントでデータテーブルにテストデータを追加します。実際のシステムでは適切なタイミングで処理してください。

C#
private void Form1_Load(object sender, EventArgs e)
{
    DataTable dt = this.dataSet11.DataTable1;

    dt.Rows.Add(new string[] { "000000", "テスト製品A" });
    dt.Rows.Add(new string[] { "000001", "テスト製品B" });
    dt.Rows.Add(new string[] { "000002", "テスト製品C" });
}
VB.NET
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
    Dim dt As DataTable = Me.DataSet11.DataTable1

    dt.Rows.Add(New String() {"000000", "テスト製品A"})
    dt.Rows.Add(New String() {"000001", "テスト製品B"})
    dt.Rows.Add(New String() {"000002", "テスト製品C"})
End Sub

コンボボックスをフォームに配置します。


コンボボックスのプロパティを設定していきます。
データソース(DataSource)はコンボボックスで表示・選択する際の元となるデータ(DataTable等)です。

メンバの表示(DisplayMember)はコンボボックスに表示される列を指定します。

値メンバ(ValueMember)はコンボボックスで選択された時に返す列を指定します。選択された列の値は SelectedValue として返されます。

選択された値(DataBindings)はコンボボックスがフォームのデータソースとバインディングされている時に選択された値(SelectedValue)を返す先(DataTableの列等)を指定します。サンプルなので返す先はなしとします。

これらのプロパティはデザイナのプロパティウィンドウやコードからも設定可能です。

この状態で実行すると、標準的なコンボボックスの完成です。
表示(DisplayMember)は製品名で、選択された値(SelectedValue)には製品IDが入ります。



このコンボボックスを、製品IDと製品名の両方が表示される様に、自前の描画部分(オーナードロー)を作成します。

コンボボックスの DrawMode を OwnerDrawFixed にします。これを忘れるとオーナードローのコードを書いても動きません。OwnerDrawVariable を使う場面は少ないと思うので割愛します。

コンボボックスの DrawItem イベントをダブルクリックしてオーナードローのコードを書きます。
C#
private void comboBox1_DrawItem(object sender, DrawItemEventArgs e)
{
    ComboBox cb = (ComboBox)sender;
    DataTable dt = this.dataSet11.DataTable1;

    float bLineX;
    Pen p = new Pen(Color.Gray);
    Brush b = new SolidBrush(e.ForeColor);

    e.DrawBackground();

    e.Graphics.DrawString(Convert.ToString(dt.Rows[e.Index]["ProductID"]), e.Font, b, e.Bounds.X, e.Bounds.Y);

    Graphics g = cb.CreateGraphics();
    SizeF sf = g.MeasureString(new string('0', dt.Rows[0]["ProductID"].ToString().Length), cb.Font);
    g.Dispose();

    bLineX = sf.Width;
    e.Graphics.DrawLine(p, bLineX, e.Bounds.Top, bLineX, e.Bounds.Bottom);

    e.Graphics.DrawString(Convert.ToString(dt.Rows[e.Index]["ProductName"]), e.Font, b, bLineX, e.Bounds.Y);

    //e.DrawFocusRectangle();
    if (Convert.ToBoolean(e.State & DrawItemState.Selected)) ControlPaint.DrawFocusRectangle(e.Graphics, e.Bounds);
}
VB.NET
Private Sub ComboBox1_DrawItem(ByVal sender As System.Object, ByVal e As System.Windows.Forms.DrawItemEventArgs) Handles ComboBox1.DrawItem
    Dim cb As ComboBox = DirectCast(sender, ComboBox)
    Dim dt As DataTable = Me.DataSet11.DataTable1

    Dim bLineX As Single
    Dim p As Pen = New Pen(Color.Gray)
    Dim b As Brush = New SolidBrush(e.ForeColor)

    e.DrawBackground()

    e.Graphics.DrawString(Convert.ToString(dt.Rows(e.Index)("ProductID")), e.Font, b, e.Bounds.X, e.Bounds.Y)

    Dim g As Graphics = cb.CreateGraphics()
    Dim sf As SizeF = g.MeasureString(New String("0"c, dt.Rows(0)("ProductID").ToString().Length), cb.Font)
    g.Dispose()

    bLineX = sf.Width
    e.Graphics.DrawLine(p, bLineX, e.Bounds.Top, bLineX, e.Bounds.Bottom)

    e.Graphics.DrawString(Convert.ToString(dt.Rows(e.Index)("ProductName")), e.Font, b, bLineX, e.Bounds.Y)

    'e.DrawFocusRectangle()
    If CBool(e.State And DrawItemState.Selected) Then ControlPaint.DrawFocusRectangle(e.Graphics, e.Bounds)
End Sub

--2011.01.24
最後の行の e.DrawFocusRectangle() は DropDownStyle が DropDownList の時しか動作しないのでコメントアウトして判定による描画にしました。フォーカスの四角形そのものを消したい場合はコメントアウトしてください。

内容としては
  1. 背景を描画
  2. 文字列(製品ID)を描画
  3. コンボボックスのフォントで描画した時の文字列サイズ(製品ID)を取得
  4. 境界線を描画
  5. 文字列を描画(製品名)
  6. フォーカスの四角形を描画

これで製品ID+境界線+製品名が表示されるので複数列として表示されます。

メンバの表示(DisplayMember)を製品IDに変更して、コンボボックスのサイズと DropDownWidth を調整すれば完成です。

もっと改良すれば、余白を計算して境界線を引いたり、色を使い分けたり、3列以上にする事も可能です。また、フォームが沢山ある場合は1つ1つにオーナードローを記述するのはとても非効率です。その場合は MultiColumnComboBox など分かり易い名前を付けて汎用的なカスタムコントロールを作成しておくと個々に書かなくて済むので楽になります。

近い将来、ほとんどのアプリが WPF や Silverlight 等の UI 分離型の解像度非依存タイプへ移行するのだろう、とは思いますが GPU の描画性能が求められる、過去の資産が活かせない等の障壁があり、まだ本格的な普及には至っておりません。

IDE の更なる進化や WPF を楽に処理できるくらい OS とハードウェアが進化したら WinForms でアプリを開発する場面は減るんでしょうかね。単純な比較にはなりませんが Windows XP の UI である Luna(ルナ)も当時は重たくてクラシックスタイルで運用する、なんて場面も見かけました。今ではサクサクですね。

■環境
OS:Microsoft Windows XP Home Edition 日本語 ServicePack 3
IDE:Microsoft Visual Studio 2005 Standard Edition 日本語 Service Pack 1
Framework:Microsoft .NET Framework Version 2.0 SP2