none
カスタムコントロール作成時のフォーカス動作設定方法について RRS feed

  • 質問

  • WPF におけるカスタムコントロール作成時の疑問点について質問させて頂きます。

    例として以下のコントロールを作成したとします(Control クラスより派生)。

      <Style x:Key="{x:Type local:ExampleControl}"
             TargetType="{x:Type local:ExampleControl}">
        <Setter Property="Focusable"
                Value="True" />
        <Setter Property="IsTabStop"
                Value="True" />
        <Setter Property="Template">
          <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:ExampleControl}">
              <StackPanel Orientation="Horizontal">
                <TextBlock Text="タイトル"
                           VerticalAlignment="Center" />
                <TextBox Text="入力してください"
                         MinWidth="120"
                         VerticalAlignment="Center" />
              </StackPanel>
            </ControlTemplate>
          </Setter.Value>
        </Setter>
      </Style>

    このままですとフォーカスを移動した際に「ExampleControl」→「内部の TextBox」とフォーカスが移動します。

    しかし、「ExampleControl」にはフォーカスを当てず、「内部の TextBox」のみにフォーカスを当てたい場合、一般的にどのように設定すべきなのでしょうか。

    「ExampleControl」の Focusable や TabStop を False に設定すれば「内部の TextBox」のみにフォーカスを当てることは可能になります。

    しかし「ExampleControl」のこれらのプロパティはコントロール全体を統括する設定であるべきで、ユーザーがこれらのプロパティを変更しても期待通りの動作にはなりません(ユーザーが外部から Focusable を設定しても、TextBox は変更されない)。

    プロパティをバインディング等したとしても設定が連動するだけで、TextBoxのみにフォーカスを当てることは実現できません。

    何か根本的な思い違いをしているような気もするのですが、このような場合どのように設定するのが望ましいのかご教授頂ければ幸いです。

    2012年10月1日 12:05

回答

  • ControlTemplateで上書きしたコントロールにのみフォーカスを移動させたい場合、トリガを追加する必要があります。
    上記の例からだとこんな感じです。

        <Style x:Key="{x:Type local:ExampleControl}"
              TargetType="{x:Type local:ExampleControl}">
            <Setter Property="Focusable"
                 Value="True" />
            <Setter Property="IsTabStop"
                 Value="True" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type local:ExampleControl}">
                        <StackPanel Orientation="Horizontal" >
                            <TextBlock Text="タイトル" VerticalAlignment="Center" />
                            <TextBox Name="textBox" 
                                     Text="入力してください"
                                     MinWidth="120"
                                     VerticalAlignment="Center"  />
                        </StackPanel>
                        
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsFocused" Value="True">
                                <Setter TargetName="textBox" Property="FocusManager.FocusedElement" Value="{Binding ElementName=textBox}"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

    元コントロール(ExampleControl)のIsFocusedプロパティをトリガにして、textBoxコントロールへフォーカスを移すようFocusManagerをセットする、ということです。
    まぁ実際にはほんの一瞬だけ、元コントロールにもフォーカスが行くわけですが、それが影響を及ぼすような実装がされていない限りこの方法で十分かと。

    以上、参考になれば幸いです。


    • 編集済み みっと 2012年10月1日 13:35
    • 回答としてマーク 紙ねんど 2012年10月2日 12:00
    2012年10月1日 13:25

すべての返信

  • ControlTemplateで上書きしたコントロールにのみフォーカスを移動させたい場合、トリガを追加する必要があります。
    上記の例からだとこんな感じです。

        <Style x:Key="{x:Type local:ExampleControl}"
              TargetType="{x:Type local:ExampleControl}">
            <Setter Property="Focusable"
                 Value="True" />
            <Setter Property="IsTabStop"
                 Value="True" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type local:ExampleControl}">
                        <StackPanel Orientation="Horizontal" >
                            <TextBlock Text="タイトル" VerticalAlignment="Center" />
                            <TextBox Name="textBox" 
                                     Text="入力してください"
                                     MinWidth="120"
                                     VerticalAlignment="Center"  />
                        </StackPanel>
                        
                        <ControlTemplate.Triggers>
                            <Trigger Property="IsFocused" Value="True">
                                <Setter TargetName="textBox" Property="FocusManager.FocusedElement" Value="{Binding ElementName=textBox}"/>
                            </Trigger>
                        </ControlTemplate.Triggers>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

    元コントロール(ExampleControl)のIsFocusedプロパティをトリガにして、textBoxコントロールへフォーカスを移すようFocusManagerをセットする、ということです。
    まぁ実際にはほんの一瞬だけ、元コントロールにもフォーカスが行くわけですが、それが影響を及ぼすような実装がされていない限りこの方法で十分かと。

    以上、参考になれば幸いです。


    • 編集済み みっと 2012年10月1日 13:35
    • 回答としてマーク 紙ねんど 2012年10月2日 12:00
    2012年10月1日 13:25
  • みっと様、ご回答ありがとうございます。

    ご提示頂いた方法で実装してみたところ、ほぼ希望通りの動作が得られました。

    ただし、ひとつ問題点がありまして、順方向でタブ遷移する場合には良いのですが、逆方向(Shift + Tab)で遷移させた場合でも ExampleControl のフォーカス時 TextBox にフォーカスが移動します。

    つまり、ExampleControl (の内部 TextBox)より前にフォーカスが移動できません。

    「ExampleControl がフォーカスを得た場合に他の要素に強制的に移動する」という方法はとてもすばらしいと思いますので、それをヒントに以下のコードを実装してみました。

    protected override void OnGotKeyboardFocus(KeyboardFocusChangedEventArgs e)
    {
        base.OnGotKeyboardFocus(e);
    
        if (this.IsKeyboardFocused)
        {
            if (e.OldFocus is DependencyObject && ContainsVisualChild(this, (DependencyObject)e.OldFocus))
            {
                this.MoveFocus(new TraversalRequest(FocusNavigationDirection.Previous));
            }
            else
            {
                this.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
    
            }
    
            e.Handled = true;
        }
    }
    
    private bool ContainsVisualChild(DependencyObject parent, DependencyObject d)
    {
        for (int index = 0; index < VisualTreeHelper.GetChildrenCount(parent); index++)
        {
            DependencyObject child = VisualTreeHelper.GetChild(parent, index);
                    
            if (child == d)
            {
                return true;
            }
            else if (ContainsVisualChild(child, d))
            {
                return true;
            }
        }
    
        return false;
    }
    

    ・親コントロールがフォーカスを得た時、遷移前の要素が親コントロールの内部要素である場合は親コントロールの前の要素に移動する。

    ・親コントロールがフォーカスを得た時、遷移前の要素が親コントロールの内部要素でない場合は親コントロールの次の要素に移動する。

    これで一応希望通りの動作を得ることができました。

    本当は XAML のみで実現できればよかったのですが、この方法では難しそうです。

    添付ビヘイビアにでもすれば転用が利きそうですので、現状はこれで実装することにします。

    大変参考になりました。ありがとうございました。

    2012年10月2日 12:02