トップ回答者
自前AutoCompleteのListBoxのスクロールバー操作でもTextBoxからフォーカスを外したくない

質問
-
C# 2017
.NET Framework 4.5
WinForms部分一致をさせたいため、TextBoxに自前のAutoComplete機能を作ろうとしています。
構造は以下の通りとして、ToolStripDropDown.Show()をすることで、あらかた作ることができました。
(ちょっとコード量が多いので掲載はしませんが・・・)
ToolStripDropDown
└ToolStripControlHost
└Panel
└ListBoxPanelを噛ませているのは、実際に表示させるコントロールをListBoxだけに限定させず、ユーザーコントロールの設置なども可能にするためです。
基本的には通常のAutoCompleteと同様の動作をさせたいため、候補の表示中でも、TextBoxに常にフォーカスが当たっている状態を保持したいです。
ListBoxの選択(上キー、下キー)は、Application.AddMessageFilter()によってTextBoxへの反応を行わせず、APIを利用してListBoxへSendMessage()させ、ListBoxの選択をさせています。ところが、ListBoxに存在するスクロールバーを操作すると、TextBoxが設置されているFormにWM_NCACTIVATEが通知され、FormのLostFocus()、TextBoxのLostFocus()が走行してしまいます。
そのため、TextBoxからフォーカスが外れ、候補の表示中にTextBoxの操作を行えなくなってしまいました。フォーカスが外れないよう、WM_NCACTIVATEを無視するような実装をWndProc()に記述すると、恐らくフォーカスが当たらないからだと思いますが、逆にListBox側のスクロールが動作しなくなりました。
WM_NCACTIVATEを無視し、クリックされた位置を判断してListBoxのTopIndexを増減させようかと思いましたが、スクロールバーのドラッグ制御、スクロールの仕方が滑らかにならないことで行き詰まりました。
スクロールバーの操作を行ってもTextBoxからフォーカスが外れなくする方法はありませんでしょうか。
上記構造でなくても構いませんが、上記にしている理由は、候補の描画がフォーム外にまで突き抜けるようにするためです。
回答
-
WPFを併用してもいいなら割と楽に
WPFユーザーコントロールのコード
<UserControl x:Class="WindowsFormsApp1.PopControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:WindowsFormsApp1" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"> <Popup x:Name="pop" > <Grid Background="LightYellow" > <ListBox Focusable="false" x:Name="listBox1" HorizontalContentAlignment="Stretch" ScrollViewer.VerticalScrollBarVisibility="Auto" Background="Transparent"> <ListBox.ItemContainerStyle> <Style TargetType="{x:Type ListBoxItem}"> <EventSetter Event="PreviewMouseDown" Handler="listBoxItem_MouseDown"/> <Setter Property="Focusable" Value="false" /> <Setter Property="Padding" Value="0" /> <Style.Triggers> <Trigger Property="IsSelected" Value="true"> <Setter Property="ContentTemplate"> <Setter.Value> <DataTemplate> <Grid Background="{DynamicResource ResourceKey={x:Static SystemColors.HighlightBrushKey}}" > <TextBlock Text="{Binding}" Foreground="{DynamicResource ResourceKey={x:Static SystemColors.HighlightTextBrushKey}}"/> </Grid> </DataTemplate> </Setter.Value> </Setter> </Trigger> </Style.Triggers> </Style> </ListBox.ItemContainerStyle> </ListBox> </Grid> </Popup> </UserControl>
namespace WindowsFormsApp1 { using System.Collections.Generic; using System.Windows.Controls; using System.Windows.Input; public partial class PopControl : UserControl { public PopControl() { InitializeComponent(); } public bool IsOpen { get { return pop.IsOpen; } set { pop.IsOpen = value; } } public IEnumerable<string> Source { get { return this.listBox1.ItemsSource as IEnumerable<string>; } set { this.listBox1.ItemsSource = value; } } public string SelectedText { get { return this.listBox1.SelectedItem as string; } } private void listBoxItem_MouseDown(object sender, MouseButtonEventArgs e) { var lbi = (ListBoxItem)sender; lbi.IsSelected = true; } public void SelectDown() { if (pop.IsOpen && listBox1.SelectedIndex < listBox1.Items.Count - 1) { listBox1.SelectedIndex++; listBox1.ScrollIntoView(listBox1.Items[listBox1.SelectedIndex]); } } public void SelectUp() { if (pop.IsOpen && listBox1.Items.Count > 0 && listBox1.SelectedIndex > 0) { listBox1.SelectedIndex--; listBox1.ScrollIntoView(listBox1.Items[listBox1.SelectedIndex]); } } public double PopWidth { get { return this.pop.Width; } set { this.pop.Width = value; } } public double PopMaxHeight { get { return this.pop.MaxHeight; } set { this.pop.MaxHeight = value; } } } }
Form
namespace WindowsFormsApp1 { using System; using System.Collections.Generic; using System.Data; using System.Drawing; using System.Linq; using System.Windows.Forms; public partial class Form1 : Form { public Form1() { InitializeComponent(); this.Deactivate += Form1_Deactivate; this.LocationChanged += Form1_LocationChanged; TextBox textBox1 = new TextBox(); textBox1.Dock = DockStyle.Top; textBox1.KeyDown += textBox1_KeyDown; textBox1.TextChanged += textBox1_TextChanged; textBox1.Leave += textBox1_Leave; this.Controls.Add(textBox1); System.Windows.Forms.Integration.ElementHost host = new System.Windows.Forms.Integration.ElementHost(); host.Location = textBox1.Location; host.Size = new Size(0, textBox1.Height);//画面下部に表示された時に避けるように pop = new PopControl() { PopWidth = 200, PopMaxHeight = 100 }; host.Child = pop; this.Controls.Add(host); dictionary = new List<string>(); Random rnd = new Random(); for (int i = 0; i < 1000; i++) { dictionary.Add(new string(Enumerable.Range(1, rnd.Next(10)).Select(_ => (char)('あ' + rnd.Next(70))).ToArray())); } } private PopControl pop; private List<string> dictionary; private bool blockFlag = false; private void textBox1_KeyDown(object sender, KeyEventArgs e) { TextBox textBox1 = (TextBox)sender; switch (e.KeyCode) { case Keys.Up: if (pop.IsOpen) { pop.SelectUp(); e.Handled = true; } break; case Keys.Down: if (pop.IsOpen) { pop.SelectDown(); e.Handled = true; } break; case Keys.Enter: if (pop.IsOpen) { blockFlag = true; textBox1.Text = pop.SelectedText; blockFlag = false; e.Handled = true; } break; case Keys.Escape: pop.IsOpen = false; break; } } private void textBox1_TextChanged(object sender, EventArgs e) { if (!blockFlag) { TextBox textBox1 = (TextBox)sender; if (string.IsNullOrEmpty(textBox1.Text)) { pop.IsOpen = false; } else { var source = dictionary.Where(_ => _.Contains(textBox1.Text)).ToArray(); pop.Source = source; pop.IsOpen = source.Length > 0; } } } private void textBox1_Leave(object sender, EventArgs e) { pop.IsOpen = false; } private void Form1_Deactivate(object sender, EventArgs e) { pop.IsOpen = false; } private void Form1_LocationChanged(object sender, EventArgs e) { if (pop.IsOpen) { pop.IsOpen = false; pop.IsOpen = true; } } } }
#AutoCompelteをカスタマイズする方法もあるけど大変
個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)
すべての返信
-
ソースコードがないのでこちらで現象の再現ができていませんが、TextBox から ListBox にフォーカスを移さないようにするのではなく、ListBox スクロール操作が終わったタイミングや ListBox の選択項目の変更が行われた直後に TextBox へフォーカスを戻すようにするのはどうでしょうか?
- 編集済み kenjinoteMVP 2018年4月26日 0:30
-
ToolStripDropDown配下自体は、TextBoxを継承したクラスに紐付けて実装はしていません。
実際はComponentクラスを継承したクラスAにToolStripDroDown配下が存在し、デザイナ上で、クラスAに、紐付けるTextBoxを選択する形を取っています。そのため、Validating, Leave, Enterなどのイベントが走行してしまうと、望んでいない動作をしてしまう場合があると思われ、更にはフォーカスが外れることにより候補が閉じてしまうため、フォーカスの再設定では正直難しいかなと思っています。
やろうとしていることは、こちらのものとほぼ同一です。
https://github.com/jgh004/CustomCompleteTextBoxこちらも、スクロールバーを操作した途端、TextBoxからフォーカスが外れてしまいます。
こちらは、CustomCompleteTextBox.csの中にすべて記述されていますが、私がやろうとしているのはTextBoxに依存しないようにしようとしてるだけです。- 編集済み takiru 2018年4月26日 4:17 同じ現象のプロジェクトリンクを追記
-
Validating, Leave, Enterなどのイベントが走行してしまうと、望んでいない動作をしてしまう場合があると思われ、更にはフォーカスが外れることにより候補が閉じてしまうため、フォーカスの再設定では正直難しいかなと思っています。
なるほど、ユーザーが ListBox 操作中でもフォーカスを移すのは難しいということですね。ListBox の規定のウィンドウプロシージャには自分自身(ListBox)にフォーカスを当てるという処理が埋め込まれているため、フォーカスを当てる処理のあるイベント(クリックイベントなど)を調査し、そのイベントが呼ばれないようにし(WndProc で base.WndProc が呼ばれないようにする)、かつ、そのイベントでフォーカスを移す以外の部分を独自で実装する必要がありそうです。ListBox を使用せず独自にリストボックス的なコントロールを作成するという手もあるかもしれません。
1 つ思い付きですが、ListBox の代わりに ContextMenuStrip を使うのはどうでしょうか?- 編集済み kenjinoteMVP 2018年4月26日 4:51
-
WPFを併用してもいいなら割と楽に
WPFユーザーコントロールのコード
<UserControl x:Class="WindowsFormsApp1.PopControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:WindowsFormsApp1" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"> <Popup x:Name="pop" > <Grid Background="LightYellow" > <ListBox Focusable="false" x:Name="listBox1" HorizontalContentAlignment="Stretch" ScrollViewer.VerticalScrollBarVisibility="Auto" Background="Transparent"> <ListBox.ItemContainerStyle> <Style TargetType="{x:Type ListBoxItem}"> <EventSetter Event="PreviewMouseDown" Handler="listBoxItem_MouseDown"/> <Setter Property="Focusable" Value="false" /> <Setter Property="Padding" Value="0" /> <Style.Triggers> <Trigger Property="IsSelected" Value="true"> <Setter Property="ContentTemplate"> <Setter.Value> <DataTemplate> <Grid Background="{DynamicResource ResourceKey={x:Static SystemColors.HighlightBrushKey}}" > <TextBlock Text="{Binding}" Foreground="{DynamicResource ResourceKey={x:Static SystemColors.HighlightTextBrushKey}}"/> </Grid> </DataTemplate> </Setter.Value> </Setter> </Trigger> </Style.Triggers> </Style> </ListBox.ItemContainerStyle> </ListBox> </Grid> </Popup> </UserControl>
namespace WindowsFormsApp1 { using System.Collections.Generic; using System.Windows.Controls; using System.Windows.Input; public partial class PopControl : UserControl { public PopControl() { InitializeComponent(); } public bool IsOpen { get { return pop.IsOpen; } set { pop.IsOpen = value; } } public IEnumerable<string> Source { get { return this.listBox1.ItemsSource as IEnumerable<string>; } set { this.listBox1.ItemsSource = value; } } public string SelectedText { get { return this.listBox1.SelectedItem as string; } } private void listBoxItem_MouseDown(object sender, MouseButtonEventArgs e) { var lbi = (ListBoxItem)sender; lbi.IsSelected = true; } public void SelectDown() { if (pop.IsOpen && listBox1.SelectedIndex < listBox1.Items.Count - 1) { listBox1.SelectedIndex++; listBox1.ScrollIntoView(listBox1.Items[listBox1.SelectedIndex]); } } public void SelectUp() { if (pop.IsOpen && listBox1.Items.Count > 0 && listBox1.SelectedIndex > 0) { listBox1.SelectedIndex--; listBox1.ScrollIntoView(listBox1.Items[listBox1.SelectedIndex]); } } public double PopWidth { get { return this.pop.Width; } set { this.pop.Width = value; } } public double PopMaxHeight { get { return this.pop.MaxHeight; } set { this.pop.MaxHeight = value; } } } }
Form
namespace WindowsFormsApp1 { using System; using System.Collections.Generic; using System.Data; using System.Drawing; using System.Linq; using System.Windows.Forms; public partial class Form1 : Form { public Form1() { InitializeComponent(); this.Deactivate += Form1_Deactivate; this.LocationChanged += Form1_LocationChanged; TextBox textBox1 = new TextBox(); textBox1.Dock = DockStyle.Top; textBox1.KeyDown += textBox1_KeyDown; textBox1.TextChanged += textBox1_TextChanged; textBox1.Leave += textBox1_Leave; this.Controls.Add(textBox1); System.Windows.Forms.Integration.ElementHost host = new System.Windows.Forms.Integration.ElementHost(); host.Location = textBox1.Location; host.Size = new Size(0, textBox1.Height);//画面下部に表示された時に避けるように pop = new PopControl() { PopWidth = 200, PopMaxHeight = 100 }; host.Child = pop; this.Controls.Add(host); dictionary = new List<string>(); Random rnd = new Random(); for (int i = 0; i < 1000; i++) { dictionary.Add(new string(Enumerable.Range(1, rnd.Next(10)).Select(_ => (char)('あ' + rnd.Next(70))).ToArray())); } } private PopControl pop; private List<string> dictionary; private bool blockFlag = false; private void textBox1_KeyDown(object sender, KeyEventArgs e) { TextBox textBox1 = (TextBox)sender; switch (e.KeyCode) { case Keys.Up: if (pop.IsOpen) { pop.SelectUp(); e.Handled = true; } break; case Keys.Down: if (pop.IsOpen) { pop.SelectDown(); e.Handled = true; } break; case Keys.Enter: if (pop.IsOpen) { blockFlag = true; textBox1.Text = pop.SelectedText; blockFlag = false; e.Handled = true; } break; case Keys.Escape: pop.IsOpen = false; break; } } private void textBox1_TextChanged(object sender, EventArgs e) { if (!blockFlag) { TextBox textBox1 = (TextBox)sender; if (string.IsNullOrEmpty(textBox1.Text)) { pop.IsOpen = false; } else { var source = dictionary.Where(_ => _.Contains(textBox1.Text)).ToArray(); pop.Source = source; pop.IsOpen = source.Length > 0; } } } private void textBox1_Leave(object sender, EventArgs e) { pop.IsOpen = false; } private void Form1_Deactivate(object sender, EventArgs e) { pop.IsOpen = false; } private void Form1_LocationChanged(object sender, EventArgs e) { if (pop.IsOpen) { pop.IsOpen = false; pop.IsOpen = true; } } } }
#AutoCompelteをカスタマイズする方法もあるけど大変
個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)