無とは何もないこと

総てがないのではなく「無」という状態があることずら

WPFでIntelliSense

http://challengeandresponse.blogspot.jp/2014/04/wpfctextbox.html
こちらのサイトでWPFでIntelliSenseを使う方法が例示されているが、いくつか不満点があるため何箇所か改良を行いました。

  • フィルターでToUpperしていなかったので、すべての要素が正しく表示されない
    →ToUpperを挿入した
  • 表示されている要素の幅次第でポップアップの幅も変わる(次図)
    →ItemsPanelTemplateを用いることで解決した

f:id:gengesa:20171114023918p:plainf:id:gengesa:20171114023913p:plain

変更後

<Window x:Class="IntelliSenseTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
        xmlns:local="clr-namespace:IntelliSenseTest"
        Title="MainWindow" Height="300" Width="500">
    <Window.DataContext>
        <local:ViewModel x:Name="vm"/>
    </Window.DataContext>
    <Window.Resources>

        <Style TargetType="local:IntelliSenseTextBox">
            <Style.Triggers>
                <Trigger Property="IsKeyboardFocusWithin" Value="True">
                    <Setter Property="TextBackground" Value="Ivory" />
                </Trigger>
            </Style.Triggers>
        </Style>
    </Window.Resources>
    <Grid>

        <TextBox AcceptsReturn="True" AcceptsTab="True" Name="queryTextBox" Margin="5"
                 VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"
                 PreviewKeyDown="queryTextBox_PreviewKeyDown"/>
        <Popup Name="popup" IsOpen="False" StaysOpen="False" MaxHeight="300" FocusVisualStyle="{x:Null}">
            <ListBox IsTextSearchEnabled="False" Name="intellisenseListBox"
                     PreviewKeyDown="intellisenseListBox_PreviewKeyDown"
                     MouseDoubleClick="intellisenseListBox_MouseDoubleClick"
                     DisplayMemberPath="Key" SelectedValuePath="Key"
                     ItemsSource="{Binding}"  FocusVisualStyle="{x:Null}"
                     >
                <ListBox.ItemsPanel>
                    <ItemsPanelTemplate>
                        <StackPanel IsItemsHost="True" />
                    </ItemsPanelTemplate>
                </ListBox.ItemsPanel>
                <ListBox.ItemContainerStyle>
                    <Style TargetType="{x:Type ListBoxItem}">
                        <Setter Property="FocusVisualStyle" Value="{x:Null}" />
                        <Setter Property="Template">
                            <Setter.Value>
                                <ControlTemplate>
                                    <StackPanel Background="{TemplateBinding Background}">
                                        <TextBlock Text="{Binding Key}" Foreground="{Binding Path=ForeColor}"/>
                                    </StackPanel>
                                </ControlTemplate>
                            </Setter.Value>
                        </Setter>
                        <Style.Triggers>
                            <Trigger Property="IsMouseOver" Value="True">
                                <Setter Property="Background" Value="Aqua" />
                            </Trigger>
                            <Trigger Property="IsSelected" Value="True">
                                <Setter Property="Background" Value="AliceBlue" />
                            </Trigger>
                        </Style.Triggers>
                    </Style>
                </ListBox.ItemContainerStyle>
            </ListBox>
        </Popup>

    </Grid>
</Window>

namespace IntelliSenseTest {
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window {
        public MainWindow() {
            InitializeComponent();
            // 項目設定
            var items = typeof(Brushes).GetProperties().Select(brush => new CandidateKeyItem() { Key = brush.Name, ForeColor = (Brush)brush.GetValue(null, null) }).ToList();
            intellisenseListBox.DataContext = items;
            this.intellisenseView = CollectionViewSource.GetDefaultView(items);
            this.intellisenseView.Filter = filterIntellisense;
        }
        /// <summary>
        /// 候補キーアイテム
        /// </summary>
        public class CandidateKeyItem {
            /// <summary>
            /// 候補キー
            /// </summary>
            public string Key { get; set; }
            /// <summary>
            /// 文字色
            /// </summary>
            public Brush ForeColor { get; set; }
        }



        /// <summary>
        /// インテリセンスコレクションビュー
        /// </summary>
        private ICollectionView intellisenseView;
        /// <summary>
        /// カレント単語
        /// </summary>
        private string currentWord = "*";





        /// <summary>
        /// クエリテキストボックスプレビューキーダウンイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void queryTextBox_PreviewKeyDown(object sender, KeyEventArgs e) {

            // インテリセンス
            if (e.Key == Key.Back || e.Key == Key.Escape) {
                popup.IsOpen = false;
                return;
            }
            if (popup.IsOpen) {
                if (e.Key == Key.Tab || e.Key == Key.Down) {
                    intellisenseListBox.Focus();
                    return;
                }
                if (e.Key == Key.Enter && intellisenseListBox.Items.Count != 0) {
                    if (intellisenseListBox.SelectedIndex == -1) {
                        intellisenseListBox.SelectedIndex = 0;
                    }
                    selectWord();
                    e.Handled = true;
                    return;
                }
            }

            // フィルタ対象外判定
            int key = (int)e.Key;
            if ((key < 34) || (85 < key) && (key < 140) || (154 < key)) {
                return;
            }

            this.Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() => {
                currentWord = getCurrentWord(queryTextBox);
                if (string.IsNullOrEmpty(currentWord)) {
                    popup.IsOpen = false;
                    return;
                }

                intellisenseView.Refresh();
                // Popupを現在のキャレットのある位置へ表示 
                popup.PlacementTarget = queryTextBox;
                popup.PlacementRectangle =
                    queryTextBox.GetRectFromCharacterIndex(queryTextBox.CaretIndex);
                // 候補が0個の時は表示しない
                popup.IsOpen = intellisenseListBox.Items.Count != 0;
                if (popup.IsOpen && intellisenseListBox.SelectedItem != null) {
                    // 既に選択したものがあればそこにスクロール
                    intellisenseListBox.ScrollIntoView(intellisenseListBox.SelectedItem);
                }
            }));
        }


        /// <summary>
        /// リストボックスプレビューキーダウンイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void intellisenseListBox_PreviewKeyDown(object sender, KeyEventArgs e) {
            // ESCAPE押下でポップアップキャンセル
            if (e.Key == Key.Escape) {
                popup.IsOpen = false;
                queryTextBox.Focus();
                return;
            }

            // 文字選択判定
            if (e.Key == Key.Enter || e.Key == Key.Tab || e.Key == Key.Space) {
                selectWord();
                e.Handled = true;
            }
        }

        /// <summary>
        /// リストビューダブルクリックイベント
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void intellisenseListBox_MouseDoubleClick(object sender, MouseButtonEventArgs e) {
            selectWord();
        }


        /// <summary>
        /// カレント文字取得
        /// </summary>
        /// <param name="textBox"></param>
        /// <returns></returns>
        private string getCurrentWord(TextBox textBox) {
            if (string.IsNullOrEmpty(textBox.Text)) {
                return string.Empty;
            }
            if (textBox.CaretIndex == 0) {
                return string.Empty;
            }

            int index = textBox.CaretIndex - 1;
            int last = textBox.Text.LastIndexOfAny(new[] { ' ', '\r', '\n', '\t' }, index) + 1;

            return textBox.Text.Substring(last, textBox.CaretIndex - last).ToUpper();
        }


        /// <summary>
        /// インテリセンスフィルター
        /// </summary>
        /// <param name="args"></param>
        /// <returns></returns>
        private bool filterIntellisense(object args) {
            bool result = false;
            var word = args as CandidateKeyItem;
            if (word != null) {
                result = word.Key.ToUpper().Contains(currentWord);
            }
            return result;
        }

        /// <summary>
        /// ワード選択
        /// </summary>
        private void selectWord() {
            if (intellisenseListBox.SelectedValue == null) {
                return;
            }
            var caretIndex = queryTextBox.CaretIndex;
            var selectedText = intellisenseListBox.SelectedValue as string;
            var topIndex = caretIndex - currentWord.Length;

            // 選択されたものを挿入
            var tmpText = queryTextBox.Text.Remove(topIndex, currentWord.Length);
            queryTextBox.Text = tmpText.Insert(topIndex, selectedText);
            queryTextBox.CaretIndex = topIndex + selectedText.Length;

            popup.IsOpen = false;
            queryTextBox.Focus();
        }

    }
}