背景
自作ツールの動作パラメータの確認画面としてListView(Details)を利用しました。
でも、パラメータであるからにはその値を変更する局面が必ず存在します。
別途編集画面を出すってのもアリはアリなんですけど、イマイチかっちょ悪い感じ。
ここはひとつ、ListViewはそのままにExcelのセル編集モードみたいな感じで編集が出来ると良いなぁ…
とか思ったりして。
方針
ListViewはSubItemの直接編集機能は持っていません。
代替策として該当エリアにWidgetを重ねて表示して、あたかも直接編集しているかの様なエクスペリエンスを与えようぜぃ、と云うのが MSの見解 の様です。
ところで、上の参照ページではListViewをサブクラス化しています。
でもさー、やりたい事はただSubItemの直接編集ちっくな動きをしたいだけなんですよねー
まぁ実際問題としてサブクラス化した方が千倍も使い勝手がよくなることは自明の理。
しかーし、ここはひとつサブクラス化を前提としないでTextBoxによる直接編集に必要な技術要素は何なのか?
辺りを纏めてみたいと思います。
要素
取り敢えず直接編集ちっくを実現するために必要な要素を挙げてみる。
- 当然ながら、ListView(Details)
- 見てくれのTextBox
- 直接編集モードってどう云う事?
ま、これ位あれば良さげ。
定義
サブクラス化は眼中にないので、取り敢えずListViewとTextBoxはフォーム上に配置されている前提で話を進めても良いよね?
となると、結局「直接編集モード」ってなんなん?
ってのを明らかにする事こそ一番重要なポイントだな。
それは…
- 然るべき位置にTextBoxが出現して
- テキスト編集を行ったのちに
- TextBoxが消えてその内容がSubItemに反映されている
と云う一連の流れ、って事にしましょう。
すると、現時点で少なくとも明確に定義されていなくてはならない事項は、
- 直接編集モードに入るトリガ
- 直接編集モードに入る際の処理
- 直接編集モードから抜けるトリガ
- 直接編集モードから抜けた時の処理
と…
それじゃこれ以降、上の4つを明文化していこう。
1) 直接編集モードに入るトリガ
これは、マウスのダブルクリックが一番適当でしょうかねぇ。
ListView
の MouseDoubleClick
イベントをとっ捕まえて編集モードに突入する、と云う事で…
2) 直接編集モードに入る際の処理
- 然るべき位置に然るべき大きさのTextBoxを用意する
- TextプロパティにSubItemのTextを移す
- TextBoxを表示状態にしてフォーカスを移す
って感じかなぁ…
ここら辺を合わせて取り敢えずコード化してみる。
private void listView_MouseDoubleClick(object sender, MouseEventArgs e) {
if(有効なカラム上でダブルクリックが起こっていれば()) {
textBox.Bounds = カラム.Bounds;
textBox.Text = カラム.Text;
textBox.Visible = true;
textBox.Focus();
}
}
3) 直接編集モードから抜けるトリガ
んー、ちょっとイメージ的に微妙だが、ここは TextBox
がフォーカスを失った時がモードを抜けるトリガとなる。
モードを抜けたからフォーカスを失うんじゃないって事が重要だな。
そして、微妙ってのはそもそも―
俺ってば何でフォーカス失くしちまったんだーっっ!!
と云うドラマがあって、実はそっちの方にフォーカス(焦点の方)をあてる方が本題っぽいんだよねー。
この人間模様については、別途考える事にしよう。
4) 直接編集モードから抜けた時の処理
これは2)のほぼ裏返し。
- TextBoxを非表示にして
- TextBoxのTextプロパティをSubItemのTextに戻す
で、コードとしてはこれ位。
private void textBox_Leave(object sender, EventArgs e) {
カラム.Text = textBox.Text;
textBox.Visible = false;
}
事由
さて、前振りしといたヤツですけど、ちょっと分類してみよう。
- TextBox自身の意志でフォーカスを手放すケース
① Enterキーが押されて、変更が確定した時
② ESCキーが押されて、変更がキャンセルされた時
- TextBoxではない外部要因に拠ってフォーカスが奪われるケース
2-1 コントローラブルな外部要因
③ TextBox上でTabキーが押された時…
④ ListViewのサイズ変更が行われた時…
⑤ ListViewのScrollバーが動かされた時…
2-2 アンコントローラブルな外部要因
⑥ TextBox以外の領域がユーザによってクリックされた時
⑦ 等…
上記ですべての要因を列挙出来ている訳ではないので、必要に応じて拡張が必要かもね。
※ コントローラブルって表現がイマイチしっくりこないけど、深く考えないでください
実装
まず、自分でフォーカスを手放す方から。
キー操作の中でモードを抜ける感じなので、 KeyPress
イベントで引っ掛ける。
private void textBox_KeyPress(object sender, KeyPressEventArgs e) {
switch (e.KeyChar) {
case (char)Keys.Enter:
listView.Focus();
e.Handled = true;
break;
case (char)Keys.Escape:
textBox.Text = カラム.Text;
listView.Focus();
e.Handled = true;
break;
}
}
お次は、コントローラブルな外部要因なんだが―
③は今回無視(コードでは対応しない)する。
Tabキーを押せばフォーカスを失うので、勝手にモード終了トリガが引かれる事になる(これはアンコントローラブルな外部要因と同じって事)。
④は ListView
の Resize
イベントハンドラで終了トリガを引く。
private void listView_Resize(object sender, EventArgs e) {
listView.Focus();
}
⑤はやっかい。
ListView
にはスクロールに関するハンドラが準備されていないのだよ。
もしそれがあれば以下のようなコードになるだろう。
private void listView_Scroll(object sender, EventArgs e) {
listView.Focus();
}
ところで、何故スクロールを察知したなら直接編集ちっくモードを抜ける必要があるのか?
それは、セルにぴったり重ねた筈の TextBox
がスクロールによって置いてけぼりにされてしまうから…
間抜けでしょ?
勿論、モードを抜けるんじゃなくて、セルに追従するって対応もアリだ。
めんどくさいから私はやらないけど。
めんどくさいと云えば(無理矢理だなぁ…)、一筋縄ではいきそうにない上記スクロールを如何に察知するのか?
これは ListView
のサブクラス化(でた!)で対応するのが一つ、二つ目の方法としてはMS謹製のWndProcHookerを利用するって手もある。
私は後者を「ふっかちゃん」と呼んでちょくちょく便利に使わせて貰っている…
閑話休題―
本稿では第3の選択肢として、特に何もしないという方針で進めたいと思う。
まぁ TextBox
以外をクリックすればモードは終了に向かって流れるので、気にしない気にしない…
(∵別稿で諸々含めてきちんとサブクラス対応しますので…)
総括
では、小出しにしてきたコードを纏めよう。
序に、上では説明してこなかった以下の点を織り込んでいきたいと思う。
-
カラム
と表記してきた部分をちゃんと実装する -
有効なカラム上でダブルクリックが起こっていれば()
とかで逃げてたとこもね -
TextBox
サイズの適正化
あ、あと前提を整理しておこう。
-
画面のイメージはこんな感じ
デザイナでListViewとTextBoxは配置しておくという事で…(どう配置するかはお任せ) -
イベントハンドラもデザイナで定義しておいてね
-
直接編集ちっく可能なカラムは「カラム2」だけ
お、もう一つ。
決して効率の良いコードではないので、そこんところは気を付ける様に。
では、どうぞ
private void Form1_Load(object sender, EventArgs e) {
textBox1.BorderStyle = BorderStyle.FixedSingle;
textBox1.Visible = false;
textBox1.Parent = listView1;
}
// カラム2の直接編集ちっくモードに入るときに、ItemのCheckが入らないようにする
private void listView1_ItemCheck(object sender, ItemCheckEventArgs e) {
Point pnt = listView1.PointToClient(Cursor.Position);
ListViewItem item = listView1.Items[e.Index];
ListViewItem.ListViewSubItem stem = item.GetSubItemAt(pnt.X, pnt.Y);
if(stem != null && stem.Bounds.Contains(pnt))
if(item.SubItems.IndexOf(stem) == 1)
e.NewValue = e.CurrentValue;
}
private void listView1_MouseDoubleClick(object sender, MouseEventArgs e) {
CurrentRow = null;
CurrentColumn = null;
Point pnt = listView1.PointToClient(Cursor.Position);
ListViewItem item = listView1.HitTest(pnt).Item;
if(item != null && item.Bounds.Contains(pnt))
CurrentRow = item;
else
return;
ListViewItem.ListViewSubItem stem = CurrentRow.GetSubItemAt(pnt.X, pnt.Y);
if(stem != null && stem.Bounds.Contains(pnt))
CurrentColumn = stem;
else
return;
if(CurrentColumnIndex != 1)
return;
Rectangle rect = CurrentColumn.Bounds;
rect.Intersect(listView1.ClientRectangle);
rect.Y -= 1;
textBox1.Bounds = rect;
textBox1.Text = CurrentColumn.Text;
textBox1.Visible = true;
textBox1.BringToFront();
textBox1.Focus();
}
private void listView1_Resize(object sender, EventArgs e) {
listView1.Focus();
}
private void textBox1_Leave(object sender, EventArgs e) {
CurrentColumn.Text = textBox1.Text;
textBox1.Visible = false;
}
private void textBox1_KeyPress(object sender, KeyPressEventArgs e) {
switch(e.KeyChar) {
case (char)Keys.Enter:
listView1.Focus();
e.Handled = true;
break;
case (char)Keys.Escape:
textBox1.Text = CurrentColumn.Text;
listView1.Focus();
e.Handled = true;
break;
}
}
ListViewItem CurrentRow;
ListViewItem.ListViewSubItem CurrentColumn;
int CurrentRowIndex { get { return (CurrentRow == null) ? -1 : CurrentRow.Index; } }
int CurrentColumnIndex { get { return (CurrentColumn == null) ? -1 : CurrentRow.SubItems.IndexOf(CurrentColumn); } }
結果
こんな感じになります。
これはこれで、そこそこ動きます。
当然、微妙な境界条件の所で妙な動きをする事はあります。(スクロールとかね、実装してないもんで)
と云う事で、次回はこれをサブクラス化してスクロールにも対応したいと思います。
っても、目新しい項目はないので、ソース載せるだけで終わってしまうかも。
で、その先にListViewItemやらListViewSubItemやらのサブクラス化にも挑戦したいと思ってます。