VC.NET ~ コンボボックスの選択変更を捕捉する

前置き

ComboBoxを使ったアプリで、ユーザが選択を変更したことを検出したい、という話しです。よくあるケースなので簡単に実現できそうですが、意外に面倒なのです。しかも、この記事、スッキリとした結論が出ません。ゞ

本文

例題

例として、次のようなプログラムを考えてみます。

  • ユーザは、ComboBoxからファイル名(foo、bar、buz)を選択する
  • 選択が変更されるたびに、選択されたファイルを読み込み、画面に表示する
  • ただし、ファイルが読めなかった場合は、メッセージボックスを出し、ComboBoxを未選択状態に戻す

SelectedIndexChangedイベント

漏れなく捕まえるには、SelectedIndexChangedイベントを使えば良いのですが、このイベントには欠点もあります。

  • プログラム内でComboBox::SelectedIndexを変更した場合でも発生してしまう
  • リスト部をドロップダウンさせた状態では、↑↓キーで選択カーソルを移動するたびに発生してしまう

多くの場合、これでも問題ないでしょう。でも今回欲しいのは、「ユーザが選択を変更し、それが確定した」というイベントです。まさに、そのために用意されたと思われるイベントがSelectionChangeCommittedです。しかし……

SelectionChangeCommittedイベント

こいつにも欠点があります。

  • リスト部をドロップダウンさせた状態で、↑↓キーで選択カーソルを移動し、EscキーやTabキーでドロップダウンを閉じた場合は発生しない

欠点というよりバグだと思うのですが、MSDNのQAでは、"by design"だと回答されています。上記のケースを捕捉したいならDropDownClosedイベントを使えとのことです。

しかし、操作によっては、SelectionChangeCommittedイベントとDropDownClosedイベントが両方とも発生するケースがあるので、余計な状態管理のコードが必要になります。今回の例題で言えば、同じファイルを2回読み込もうとしたり、エラーのメッセージボックスを2回出してしまうようなことは避けなければなりません。

イベント発生の実験

いろんな操作を行ったときに、どんなイベントが発生するか実験してみました。

調べたイベントは以下の4つです。

イベント略称
TextChangedTC
SelectedIndexChangedIX
SelectionChangeCommittedCom
DropDownClosedClose

ComboBoxには3つのスタイル(DropDownStyleプロパティ)がありますが、今回は以下の2つを実験しました。

DropDown
リスト部をドロップダウンできる。さらに、編集可能なエリアも持つ。
DropDownList
リスト部をドロップダウンできる。

紛らわしいので、前者をエディットタイプ、後者をリストタイプと呼びましょう。

ComboBoxには、あらかじめ、3つの選択肢を登録しておきます。

  • foo
  • bar
  • buz

実験結果

数字の順にイベントが発生します。丸数字は1度だけ、括弧()の数字は選択カーソルを移動させるたびに発生することを表しています。

エディットタイプの場合

操作 TCIXComClose
Aマウスで1つを選択
BComboBox上で↓キーを押す
CAlt+↓でドロップダウンし、↓↑で選択カーソルを移動してEscで閉じる (1)(2)
DAlt+↓でドロップダウンし、↓↑で選択カーソルを移動してTabで閉じる (1)④(2)
EAlt+↓でドロップダウンし、↓↑で選択カーソルを移動してEnterで閉じる(1)(2)
Fマウスでドロップダウンし、そのまま閉じる ※1
GAlt+↓でドロップダウンし、そのままEscで閉じる
HプログラムでSelectedIndexプロパティを-1に変更
IプログラムでSelectedIndexプロパティを0に変更
JプログラムでTextプロパティを"foo"に変更 ①②
KプログラムでTextプロパティを"hoge"に変更

リストタイプの場合

操作 TCIXComClose
Aマウスで1つを選択
BComboBox上で↓キーを押す
CAlt+↓でドロップダウンし、↓↑で選択カーソルを移動してEscで閉じる (1)
DAlt+↓でドロップダウンし、↓↑で選択カーソルを移動してTabで閉じる (1)
EAlt+↓でドロップダウンし、↓↑で選択カーソルを移動してEnterで閉じる(1)
Fマウスでドロップダウンし、そのまま閉じる ※1
GAlt+↓でドロップダウンし、そのままEscで閉じる
HプログラムでSelectedIndexプロパティを-1に変更
IプログラムでSelectedIndexプロパティを0に変更
JプログラムでTextプロパティを"foo"に変更 ①②
KプログラムでTextプロパティを"hoge"に変更 ※2

※1: リスト部上でマウスを(クリックせずに)動かし選択カーソルを移動させてますが、その間、イベントは発生しません。

※2: "hoge"という選択肢は無いのでTextプロパティは変わりませんが、TextChangedイベントは発生します。

考察

捕まえたいのは、AからEまでの操作です。

MSの提案通り、基本的にはSelectionChangeCommittedイベントで待ち、CとDに備えるためにDropDownCloseイベントも見る、というのが素直な気がします。ただし、AとEでは両方とも発生するので、2重捕捉を見分けられるように工夫する必要があります。さらに、FとGも捕まえてしまうので、本当にSelectedIndexが変化したのかどうかをチェックする必要もあります。コードにすると、こんな感じでしょうか。

TBD

リストタイプに限れば、TextChangedイベントも使えそうですね。TextChangedだけではBが漏れてしまうので、SelectionChangeCommittedイベントも見る必要があります。幸いBの場合はリスト部がドロップダウンしてないので、2重捕捉を避けるのは容易でしょう。また、H~KでもTextChangedイベントが発生するので、対策が必要です。プログラムでSelectedIndexやTextプロパティを変更するときは、いったんイベントハンドラを登録解除する、というのはどうでしょうか。コードにすると、こんな感じです。

TBD

さらに

今回の例題では、ファイルが存在しない場合は、イベントハンドラの中でメッセージボックスを出すようになっています。例えば、ファイルbuzが存在しないとしましょう。Eの操作方法でbuzを選ぶと、Windows XPでは期待通りに動くのですが、Windows7ではメッセージボックスが2回出てしまいました。

コードは、こんな感じです。SelectionChangeCommittedとDropDownClosedイベントを使った実装になっています。

TBD

デバッグしてみたところ、DropDownClosedイベントが来たとき、SelectedIndexが2になっていました。タイミング的には、それより先にSelectedIndexを-1にしているはずなのですが…。マルチスレッドの問題でもないし、不思議です。これを回避するためには……。みっともないので、お見せできません。

ちなみに、前述の表で示した実験結果はWindows XPとWindows7で同じでした。来るイベントの種類や順序は同じだけど、プロパティとか細かい状態管理のレベルでは違いがある、ということですね。どのイベントも、機能仕様的には曖昧なところがあり、実装仕様がOSごとに多少異なるのは当たり前、くらいに思っておいた方が良いかもしれません。新しいOSが出るたびに実験をやり直す感じで。

Last modified:2012/01/26 19:06:48
Keyword(s):
References:[.NETアプリ開発]
This page is frozen.