none
自前AutoCompleteのListBoxのスクロールバー操作でもTextBoxからフォーカスを外したくない RRS feed

  • 質問

  • C# 2017
    .NET Framework 4.5
    WinForms

    部分一致をさせたいため、TextBoxに自前のAutoComplete機能を作ろうとしています。

    構造は以下の通りとして、ToolStripDropDown.Show()をすることで、あらかた作ることができました。
    (ちょっとコード量が多いので掲載はしませんが・・・)
    ToolStripDropDown
     └ToolStripControlHost
       └Panel
         └ListBox

    Panelを噛ませているのは、実際に表示させるコントロールを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からフォーカスが外れなくする方法はありませんでしょうか。
    上記構造でなくても構いませんが、上記にしている理由は、候補の描画がフォーム外にまで突き抜けるようにするためです。

    2018年4月25日 7:22

回答

  • 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!)


    • 編集済み gekkaMVP 2018年4月26日 14:59 位置補正のためのコード追加
    • 回答としてマーク takiru 2018年9月16日 1:37
    2018年4月26日 11:27

すべての返信

  • ソースコードがないのでこちらで現象の再現ができていませんが、TextBox から ListBox にフォーカスを移さないようにするのではなく、ListBox スクロール操作が終わったタイミングや ListBox の選択項目の変更が行われた直後に TextBox へフォーカスを戻すようにするのはどうでしょうか?
    2018年4月25日 9:56
  • 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 同じ現象のプロジェクトリンクを追記
    2018年4月26日 3:48
  • Validating, Leave, Enterなどのイベントが走行してしまうと、望んでいない動作をしてしまう場合があると思われ、更にはフォーカスが外れることにより候補が閉じてしまうため、フォーカスの再設定では正直難しいかなと思っています。

    なるほど、ユーザーが ListBox 操作中でもフォーカスを移すのは難しいということですね。ListBox の規定のウィンドウプロシージャには自分自身(ListBox)にフォーカスを当てるという処理が埋め込まれているため、フォーカスを当てる処理のあるイベント(クリックイベントなど)を調査し、そのイベントが呼ばれないようにし(WndProc で base.WndProc が呼ばれないようにする)、かつ、そのイベントでフォーカスを移す以外の部分を独自で実装する必要がありそうです。ListBox を使用せず独自にリストボックス的なコントロールを作成するという手もあるかもしれません。

    1 つ思い付きですが、ListBox の代わりに ContextMenuStrip を使うのはどうでしょうか?
    2018年4月26日 4:16
  • 原理的な話になりますが、

    (1)異なるHWNDを持つ二つのコントロールに対して、同時にキーボードフォーカスを与えることはできません。
      これは入力デバイスとしてのキーボードが唯一であるという想定のもとにOSが構築されているからです。

    従って、

    (2)所望されている動作を、HWNDを持つ既存のコントロールの組み合わせで実装することは極めて困難であると予測できます。

    一般に、OSの想定する以外の動作をさせたい場合には、
    その動作の全てを実装した新しい単位コントロールを作成する方法が最短距離となります。

    2018年4月26日 5:17
  • 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!)


    • 編集済み gekkaMVP 2018年4月26日 14:59 位置補正のためのコード追加
    • 回答としてマーク takiru 2018年9月16日 1:37
    2018年4月26日 11:27
  • 皆様ありがとうございます。

    まずは、いただいた方法論でいくつか試してみて調査しています。

    2018年4月27日 0:28