InputMan for WPF の定義済みショートカットコマンドと編集可能 ComboBox の問題
現在 GrapeCity さんの InputMan for WPF を使って開発してるのですが、様々な定義済みショートカットコマンドが提供されています。WPF の標準コントロールに対応しているショートカットコマンドでは以下の二つがあります。
プロパティ | 説明 |
NextControl | 次のコントロールにフォーカスを移動します。 |
PreviousControl | 前のコントロールにフォーカスを移動します。 |
これ結構便利で、たとえば Enterキーで次のコントロールへ移動、Shift + Enter で前のコントロールに移動するには以下のように実装します。
<TextBox> <TextBox.InputBindings> <KeyBinding Key="Enter" Command="im:ControlNavigationCommands.NextControl"/> <KeyBinding Key="Enter" Modifiers="Shift" Command="im:ControlNavigationCommands.PreviousControl"/> </TextBox.InputBindings> </TextBox>
エンドユーザーから「上下キーでコントロールを移動できるようにして欲しい」との要望があったので、Application.xaml で標準コントロールのスタイルにショートカットコマンドを設定しました。ちなみに UpDown キーでコントロールを移動する場合、XAML は以下のように記述しています。
<!-- CheckBox Style --> <Style TargetType="{x:Type CheckBox}" > <Setter Property="im:InputBindingHelper.InputBindings"> <Setter.Value> <InputBindingCollection> <KeyBinding Key="Up" Command="im:ControlNavigationCommands.PreviousControl"/> <KeyBinding Key="Down" Command="im:ControlNavigationCommands.NextControl"/> </InputBindingCollection> </Setter.Value> </Setter> </Style>
ComboBox.IsEditable = true の問題
しかしここで問題が発生しました。Button・CheckBox・RadioButton・TextBox・PasswordBox 等の標準コントロールは問題なく動作し、ComboBox も標準の IsEditable = false なら動作しますが、IsEditable = true にすると全く UpDown キーに反応しません。以前から色々引っかかってるのですが、どうも 標準 ComboBox が怪しすぎる・・・(-ω-;
参考記事:
WPF の ComboBox のIME を無効にできない
WPF : ComboBox.IsEditable プロパティを True にすると、IsTabStop を False にしてもタブストップが有効のまま
この問題、フィードバックに投げるかいろいろ考えたのですが、サードパーティ製品使ってるから Connect じゃまともに扱ってくれないだろうし、フォーラムに投げるのも違う気がする、InputMan for WPF の問題とは思えないので GrapeCity さんに投げるのもなんか違う気がするんで困ってたのですが、先日たまたま id:yamaki さんにお会いする機会があってこの事お伝えしたら、ぜひメールで教えて下さいとこころよく言ってもらえました。ただしメールだと長くなり過ぎるし、技術情報として参考になる方いるかもしれないので、まずはブログで公開し情報整理してから報告させて頂きたいと思います。
まず XAML で書いてみる
ComboBox を IsEditable = true で編集可能にすると PART_EditableTextBox という名前の TextBox 要素が入力ターゲットになるのは以前から掴んでいたので、ControlTemplate を展開して PART_EditableTextBox にショートカットコマンドを記述します。以下抜粋した XAML です。
<TextBox x:Name="PART_EditableTextBox" Grid.Column="1" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" IsReadOnly="{Binding IsReadOnly, RelativeSource={RelativeSource TemplatedParent}}" IsTabStop="{Binding IsTabStop, RelativeSource={RelativeSource TemplatedParent}}" Margin="{TemplateBinding Padding}" Style="{StaticResource ComboBoxEditableTextBox}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"> <im:InputBindingHelper.InputBindings> <InputBindingCollection> <KeyBinding Key="Up" Command="im:ControlNavigationCommands.PreviousControl"/> <KeyBinding Key="Down" Command="im:ControlNavigationCommands.NextControl"/> </InputBindingCollection> </im:InputBindingHelper.InputBindings> </TextBox>
結果・・・ダメ、UpDown キーに反応しない。orz
では PART_EditableTextBox がスタイルで使ってるリソース、「ComboBoxEditableTextBox」にショートカットコマンドを設定してみます。
<Style x:Key="ComboBoxEditableTextBox" TargetType="{x:Type TextBox}"> <Setter Property="OverridesDefaultStyle" Value="true"/> <Setter Property="AllowDrop" Value="true"/> <Setter Property="MinWidth" Value="0"/> <Setter Property="MinHeight" Value="0"/> <Setter Property="FocusVisualStyle" Value="{x:Null}"/> <Setter Property="ScrollViewer.PanningMode" Value="VerticalFirst"/> <Setter Property="Stylus.IsFlicksEnabled" Value="True"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type TextBox}"> <ScrollViewer x:Name="PART_ContentHost" Background="Transparent" Focusable="false" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden"/> </ControlTemplate> </Setter.Value> </Setter> <Setter Property="im:InputBindingHelper.InputBindings"> <Setter.Value> <InputBindingCollection> <KeyBinding Key="Up" Command="im:ControlNavigationCommands.PreviousControl"/> <KeyBinding Key="Down" Command="im:ControlNavigationCommands.NextControl"/> </InputBindingCollection> </Setter.Value> </Setter> </Style>
駄目ですねぇ・・・これも反応しません・・・
ビヘイビアを使ってみる
苦肉の策でビヘイビアを使ってみます。まず子要素検索クラスを用意します。
using System; using System.Windows; using System.Windows.Media; /// <summary> /// UIElement 子要素検索ヘルパークラス /// </summary> public static class UIChildFinder { /// <summary> /// 子要素検索の拡張メソッドです。 /// </summary> public static DependencyObject FindChild (this DependencyObject reference, Type childType, string childName) { DependencyObject foundChild = null; if (reference != null) { int childrenCount = VisualTreeHelper.GetChildrenCount(reference); for (int i = 0; i < childrenCount; i++) { var child = VisualTreeHelper.GetChild(reference, i); // 子要素が検索する方でなければ if (child.GetType() != childType) { // 再帰的に階層をたどります。 foundChild = FindChild(child, childType, childName); if (foundChild != null) break; } else if (!string.IsNullOrEmpty(childName)) { var frameworkElement = child as FrameworkElement; // 子要素の名前が検索条件に設定されてるなら if (frameworkElement != null && frameworkElement.Name == childName) { // 子要素がリクエストした名前にヒットした foundChild = child; break; } } else { // 子要素が見つかった foundChild = child; break; } } } return foundChild; } }
んで、ビヘイビアは以下のように実装します。PART_EditableTextBox の KeyDown でフォーカスを飛ばします。
using System; using System.Windows; using System.Windows.Controls; using System.Windows.Input; /// <summary> /// ComboBox 添付ビヘイビア /// </summary> public static class ComboBoxBehaviors { /// <summary> /// UpDownキーでコントロールを移動します。 /// </summary> public static readonly DependencyProperty IsUpDownMoveFocusProperty = DependencyProperty.RegisterAttached( "IsUpDownMoveFocus", typeof(bool), typeof(ComboBoxBehaviors), new UIPropertyMetadata(false, IsUpDownMoveFocusChanged) ); [AttachedPropertyBrowsableForType(typeof(ComboBox))] public static bool GetIsUpDownMoveFocus(DependencyObject obj) { if (obj == null) { throw new ArgumentNullException("obj"); } return (bool)obj.GetValue(IsUpDownMoveFocusProperty); } [AttachedPropertyBrowsableForType(typeof(ComboBox))] public static void SetIsUpDownMoveFocus(DependencyObject obj, bool value) { if (obj == null) { throw new ArgumentNullException("obj"); } obj.SetValue(IsUpDownMoveFocusProperty, value); } private static void IsUpDownMoveFocusChanged (DependencyObject sender, DependencyPropertyChangedEventArgs args) { var comboBox = sender as ComboBox; if (comboBox == null) return; comboBox.Loaded += (s, e) => { var textBox = comboBox.FindChild(typeof(TextBox), "PART_EditableTextBox"); if (textBox == null) return; // 設定された値を見てイベントを登録・削除 ((TextBox)textBox).KeyDown -= OnKeyDownIsUpDownMoveFocus; var newValue = (bool)args.NewValue; if (newValue) { ((TextBox)textBox).KeyDown += OnKeyDownIsUpDownMoveFocus; } }; } static void OnKeyDownIsUpDownMoveFocus(object sender, KeyEventArgs e) { if (e.Key == Key.Up) { TraversalRequest tRequest = new TraversalRequest(FocusNavigationDirection.Previous); UIElement keyboardFocus = Keyboard.FocusedElement as UIElement; if (keyboardFocus != null) { keyboardFocus.MoveFocus(tRequest); } e.Handled = true; } if (e.Key == Key.Down) { TraversalRequest tRequest = new TraversalRequest(FocusNavigationDirection.Next); UIElement keyboardFocus = Keyboard.FocusedElement as UIElement; if (keyboardFocus != null) { keyboardFocus.MoveFocus(tRequest); } e.Handled = true; } } }
結果・・・これもダメ!><
var textBox = comboBox.FindChild(typeof(TextBox), "PART_EditableTextBox");
で、PART_EditableTextBox のインスタンスは取得できるのですが、ComboBox にフォーカスを当て UpDownキーを入力しても OnKeyDownIsUpDownMoveFocus が呼ばれない!>< でさらに調べたら、他のキーだとイベントが発生するのですが、方向キーだけ思いっきりスルーしてるみたい。
で、さらに XAML を試していたら Shift + Enter の組み合わせは反応した。謎です・・・(^^;
<TextBox x:Name="PART_EditableTextBox" Grid.Column="1" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" IsReadOnly="{Binding IsReadOnly, RelativeSource={RelativeSource TemplatedParent}}" IsTabStop="{Binding IsTabStop, RelativeSource={RelativeSource TemplatedParent}}" Margin="{TemplateBinding Padding}" Style="{StaticResource ComboBoxEditableTextBox}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"> <im:InputBindingHelper.InputBindings> <InputBindingCollection> <KeyBinding Key="Enter" Command="im:ControlNavigationCommands.NextControl"/> <KeyBinding Key="Enter" Modifiers="Shift" Command="im:ControlNavigationCommands.PreviousControl"/> <KeyBinding Key="Up" Command="im:ControlNavigationCommands.PreviousControl"/> <KeyBinding Key="Down" Command="im:ControlNavigationCommands.NextControl"/> </InputBindingCollection> </im:InputBindingHelper.InputBindings> </TextBox>