チェックボックス付きのコンボボックス

前々から作りたかったチェックボックス付きのコンボを作ってみた。1年前にMSDNに偶然サンプルコードを見つけてから試行錯誤の末にようやく使えそうになってきたので、まとめてみる。



MSDNで見つけたサンプルコードはこちらのページ。

ToolStripDropDown クラス

ここの2番目のサンプルが、コンボボックスからツリービューが表示されると云うコードだった。試しに動かしてみるとこんな感じ。


おお、いいじゃないの。MSDNありがとう。



ツリービューをCheckedListBoxに張り替えてみた。


リストの幅が狭かったりするがそれは置いておいて、使ってみると微妙なところが3つあった。

  1. リストが表示されてから「one」などの項目をクリックしても、なにも反応がない。もう一度クリックすると、項目が選択できる。
  2. リストが表示されてからすぐにコンボボックスの本体部分をクリックすると、リストが消える。しかし、リストの項目を選択した後にコンボボックスの本体部分をクリックしても、リストが消えない(再描画されている様子)。動作が違う。
  3. コンボボックスとリストの間にある青い太い線は何?
調べてみると原因はひとつで、コンボボックスの本来のリストも表示されていて(青い太い線がそれ)、どこかをクリックするまで消えないためだった。

青い線が表示されているうちは、チェックボックスのリストは反応しない。クリックすると青い線が消えて、チェックボックスのリストを選択できる、(1の理由)

青い線が表示されているときは、コンボボックスの本体部分をクリックすると青い線が消える。と同時にチェックボックスのリストも消える。逆に、青い線が消えているときにコンボボックスの本体部分をクリックすると、青い線が表示される。と同時にチェックボックスのリストも表示される。(2の理由)

うーん、文章で書くとわかりにくいので、コンボボックスにデータを1行追加してみよう。こうなる。


「XYZ」という1行をコンボに追加してみると、コンボの本来のリストが現れる。この「XYZ」の無いのが、先の図の青い太い線でした。

コンボボックスの本来のリストが、後付けのチェックボックスのリストの動作を邪魔して、微妙な動作を引き起こしていたわけだ。


この微妙な動作を解決するためには、コンボボックスの本来のリストを表示しないようにすればよい。ただ、
  • コンボにデータを追加せずに空にしてリストを見せない
  • リストの高さを1にして(DropDownHeight=1)リストを見せない
という「見えなきゃいいよね?」的方法は、この場合は解決にならない。

では別の方法を。

コンボボックスにはリストの表示状態を示すプロパティがあって、これをfalseにすれば隠せる(DroppedDown)。しかし、このプロパティをfalseにするタイミングが難しい。

MSDNのサンプルコードには、チェックリストを表示するメソッドがあるので、その末尾にプロパティの状態を出力させてみたところ、この時点でも、まだfalseだった。

private void ShowDropDown()
{
    if (dropDown != null)
    {
        ckListHost.Width = DropDownWidth;
        ckListHost.Height = DropDownHeight;
        dropDown.Show(this, 0, this.Height);

        System.Diagnostics.Debug.Print("ShowDropDown: {0} {1}", this.DroppedDown, "");  // まだfalse
    }
}

つまり、この時点でプロパティをfalseにしても、事態はなにも変わらない。

それなら、チェックボックスのリストを開いたあとならどうかと、試してみたが、ここでもfalseであった。

public mmCheckedComboBox()
{
    (略)
    dropDown.Opened += DropDown_Opened;
}

private void DropDown_Opened(object sender, EventArgs e)
{
    System.Diagnostics.Debug.Print("DropDown_Opened: {0} {1}", this.DroppedDown, "");
}

一体、いつtrueになるのだろうか、、、



MSDNのサンプルコードには「WndProc」メソッドがあって、そこで「ドロップダウンしろ」というメッセージを受け取ってShowDropDownメソッドを呼ぶという流れが書かれている(ようだ)。

ここを改造して、クリックされたらShowDropDownメソッドを呼ぶという風に変えると、コンボボックスのリストは表示されないかもしれない。
ウインドウズのメッセージはよくわからないのだけど、調べて、こんな感じで書き変えてみた。

protected override void WndProc(ref Message m)
{
    if (m.Msg == WM_LBUTTONDOWN)        // WM_LBUTTONDOWN = 0x201
    {
        ShowDropDown();
        return;
    }
    base.WndProc(ref m);
}

動かすとこうなった。おお、良い感じ。


青い太い線が消えて、チェックリストが表示されて「one」などの項目をクリックするときちんと反応する。ただ、コンボボックスの本体をクリックしたときの動作は、いつクリックしてもチェックリストが表示されるに変わってしまった。動作は同じになったが、ここは表示ではなく消えて欲しいところ。



ShowDropDownメソッドの中で、チェックボックスのリストが表示されていたら何もしないで抜ける、、、というようにしてみたが何も変わらなかった。

落ち着いて考えると、コンボボックスをクリックしたときには、アプリは以下のように動作しているだとうから、
  1. チェックボックスのリストが表示されていたら(自動的に)消える。
  2. チェックボックスのリストを表示する。
ShowDropDownメソッドは2の動作そのもので、この時点ではチェックボックスのリストは消えているのだから、何も変わらないのは当たり前と云えば当たり前。


仕方がないので、チェックボックスのリストが消えてから0.3秒以内なら何もしないで抜ける、という具合に変えてみた。我ながら、かなり苦し紛れだな。

まず、チェックボックスのリストが消えた時刻を記録する。

public mmCheckedComboBox()
{
(略)
    dropDown.Closed += DropDown_Closed;
}

private void DropDown_Closed(object sender, ToolStripDropDownClosedEventArgs e)
{
    listClosedTime = DateTime.Now;
}


次にShowDropDownメソッドで0.3秒経過したかをチェックして、過ぎていなければチェックボックスのリストを表示しないようにする。

private void ShowDropDown()
{
    if (dropDown != null)
    {
        if (DateTime.Now < listClosedTime.AddMilliseconds(300.0))
        {
            /// リストが消えた直後にクリックされた場合は、無視する
            return;
        }

        ckListHost.Width = DropDownWidth;
        ckListHost.Height = DropDownHeight;
        dropDown.Show(this, 0, this.Height);

        System.Diagnostics.Debug.Print("ShowDropDown: {0} {1}", this.DroppedDown, dropDown.Visible);
    }
}

ありがたいことに良い感じで動くようになった。やれやれ。



リストの横幅や、選択した値の表示、キー入力(F4とかALT+↓)など実際に使うには、まだまだ調整が必要だが、それはいつも使っているノウハウでなんとかなるだろう。

ということで、この項目はこれでおしまい。

ソースコード

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Security.Permissions;


namespace checkedComboBoxA
{
    public partial class FormMsdnSampleCheckedBox : Form
    {
        public FormMsdnSampleCheckedBox()
        {
            InitializeComponent();

            mmCheckedComboBox treeCombo = new mmCheckedComboBox();
            treeCombo.CheckedListBox.Items.Add("one");
            treeCombo.CheckedListBox.Items.Add("two");
            treeCombo.CheckedListBox.Items.Add("three");

            treeCombo.DropDownStyle = ComboBoxStyle.DropDownList;
            treeCombo.Items.Add("XYZ");
            this.Controls.Add(treeCombo);
        }

        private void FormMsdnSample_Load(object sender, EventArgs e)
        {

        }

        [SecurityPermissionAttribute(
            SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
        public class mmCheckedComboBox : ComboBox
        {
            ToolStripControlHost ckListHost;
            ToolStripDropDown dropDown;
            DateTime listClosedTime;

            public mmCheckedComboBox()
            {
                CheckedListBox ckList = new CheckedListBox();
                ckList.BorderStyle = BorderStyle.None;
                ckListHost = new ToolStripControlHost(ckList);
                dropDown = new ToolStripDropDown();
                dropDown.Items.Add(ckListHost);

                dropDown.Opened += DropDown_Opened;
                dropDown.Closed += DropDown_Closed;
            }

            private void DropDown_Closed(object sender, ToolStripDropDownClosedEventArgs e)
            {
                listClosedTime = DateTime.Now;
            }

            private void DropDown_Opened(object sender, EventArgs e)
            {
                System.Diagnostics.Debug.Print("DropDown_Opened: {0} {1}", this.DroppedDown, "");
            }

            public CheckedListBox CheckedListBox
            {
                get { return ckListHost.Control as CheckedListBox; }
            }


            private void ShowDropDown()
            {
                if (dropDown != null)
                {
                    if (DateTime.Now < listClosedTime.AddMilliseconds(300.0))
                    {
                        /// リストが消えた直後にクリックされた場合は、無視する
                        return;
                    }

                    ckListHost.Width = DropDownWidth;
                    ckListHost.Height = DropDownHeight;
                    dropDown.Show(this, 0, this.Height);

                    System.Diagnostics.Debug.Print("ShowDropDown: {0} {1}", this.DroppedDown, dropDown.Visible);
                }
            }

            private const int WM_USER = 0x0400,
                              WM_REFLECT = WM_USER + 0x1C00,
                              WM_COMMAND = 0x0111,
                              WM_LBUTTONDOWN = 0x201,
                              CBN_DROPDOWN = 7;

            public static int HIWORD(int n)
            {
                return (n >> 16) & 0xffff;
            }

            protected override void WndProc(ref Message m)
            {
                if (m.Msg == WM_LBUTTONDOWN)        // WM_LBUTTONDOWN = 0x201
                {
                    ShowDropDown();
                    return;
                }
                if (m.Msg == (WM_REFLECT + WM_COMMAND))
                {
                    if (HIWORD((int)m.WParam) == CBN_DROPDOWN)
                    {
                        System.Diagnostics.Debug.Print("### message: {0}", m.ToString());
                        ShowDropDown();
                        return;
                    }
                }
                base.WndProc(ref m);
            }

            protected override void Dispose(bool disposing)
            {
                if (disposing)
                {
                    if (dropDown != null)
                    {
                        dropDown.Dispose();
                        dropDown = null;
                    }
                }
                base.Dispose(disposing);
            }
        }
    }
}


コメント

このブログの人気の投稿

varchar をデータ型 numeric に変換中に、算術オーバーフロー エラーが発生しました。