新しもの好きプログラマの耳より情報ブログ

仕事でもあるプログラミングについて役に立ちそうな情報を発信していこうというブログです。役に立たなそうな情報はfacebookで。

WPFのタブオーダーを、Panel単位で並べ替える方法

概要

WPFでPanelがいくつも並ぶ画面を作った場合に、Panel単位でタブオーダーを並べ替えたいという状況はよくあると思います。とはいえ、全てのコントロールにタブオーダー指定をするのは避けたい。WPFではそれを実現できるタブオーダー指定の方法があり、上手く使うととても便利なので、それを紹介します。

最初に結論まとめ

Panel(などの親要素)に対してKeyboardNavigation.TabIndexでタブオーダーを指定した上で、さらにKeyboardNavigation.TabNavigation="Local"を指定して子要素のタブオーダーを独立させます。

こうすることで、子要素にはタブオーダーを設定せずに、Panelなどの親要素にだけタブオーダーを設定して並べ替えることができます。

使えるシーン

Panel単位でタブオーダーを並べ替えたいシーンというのは、どういうものかというと・・・

例えばウィザード風の画面で、画面を開いた時には下部の「次へ・前へ」ボタンがあるところにフォーカスを当てたいが、タブ遷移すると画面の最上部の入力欄へフォーカスが行くようにしてほしい、などです。

image.png (1,2,3の順でタブ遷移したい)

これは全てのコントロールに個別のタブオーダーを振れば実現できますが、コントロールの並べ替えや挿入が難しくなるため、保守性(この場合変更容易性)がぐっと下がります。MFCやFormsなどではそれを避けるために、「パネルには100の倍数のタブオーダーを振り、パネルの子要素には101,102など小さい桁でタブオーダーを振る」というようなハックを頑張ってやった人もいるのではないでしょうか。

実現方法

WPFではこのような場合、Panelだけにタブオーダーを振って一工夫することで、その動作を実現できます。子要素にタブオーダーを書かなくていいので、保守性はバッチリです。

具体的なコードを書いてみます。上の画面を実現するxamlは色々と属性が多いので、説明に関係ない属性を削って、次のコードをスタート地点にします。

<DockPanel>
    <Border>
        <DockPanel>
            <TextBlock Text="入力欄1-1"></TextBlock>
            <TextBox Width="100"></TextBox>
            <TextBlock Text="入力欄1-2"></TextBlock>
            <TextBox Width="100"></TextBox>
        </DockPanel>
    </Border>
    <Border>
        <DockPanel>
            <TextBlock Text="入力欄2-1"></TextBlock>
            <TextBox Width="100"></TextBox>
            <TextBlock Text="入力欄2-2"></TextBlock>
            <TextBox Width="100"></TextBox>
        </DockPanel>
    </Border>
    <DockPanel>
        <Button Content="前へ"></Button>
        <Button Content="次へ"></Button>
    </DockPanel>
</DockPanel>

この状態だと、タブオーダーは次の画像の赤字のようになります。

image.png

「前へ・次へ」ボタンがあるパネルに先にタブが行くようにしたいので、単にパネルに対してタブオーダーを設定してみましょう。一番下のボタンのパネルのTabIndexを0にしてみます。

<DockPanel>
    <Border KeyboardNavigation.TabIndex="1">
        <DockPanel>
            <TextBlock Text="入力欄1-1"></TextBlock>
            <TextBox Width="100"></TextBox>
            <TextBlock Text="入力欄1-2"></TextBlock>
            <TextBox Width="100"></TextBox>
        </DockPanel>
    </Border>
    <Border KeyboardNavigation.TabIndex="2">
        <DockPanel>
            <TextBlock Text="入力欄2-1"></TextBlock>
            <TextBox Width="100"></TextBox>
            <TextBlock Text="入力欄2-2"></TextBlock>
            <TextBox Width="100"></TextBox>
        </DockPanel>
    </Border>
    <DockPanel KeyboardNavigation.TabIndex="0">
        <Button Content="前へ"></Button>
        <Button Content="次へ"></Button>
    </DockPanel>
</DockPanel>

するとどうなるか?タブの動きは前のものと変わりません。なぜそうなるかというと、これは見た目には分かりづらいのですが・・・次のように「まずTabIndexが設定されたパネル上を設定順にタブ遷移し、その後にTabIndexが設定されていないところを遷移する」となっているためです。パネルにはフォーカスが当たらないので1,2,3はスキップされ、結果的に上から順番にタブオーダーが並びます。これでは狙いと違います。

image.png

ではどうすれば良いか?タブオーダーを設定したパネルに、その子要素のタブオーダーをどうするかのオプションを、KeyboardNavigation.TabNavigationで設定します。この目的の場合、設定値はLocalです。

<DockPanel>
    <Border KeyboardNavigation.TabNavigation="Local" KeyboardNavigation.TabIndex="1">
        <DockPanel>
            <TextBlock Text="入力欄1-1"></TextBlock>
            <TextBox Width="100"></TextBox>
            <TextBlock Text="入力欄1-2"></TextBlock>
            <TextBox Width="100"></TextBox>
        </DockPanel>
    </Border>
    <Border KeyboardNavigation.TabNavigation="Local" KeyboardNavigation.TabIndex="2">
        <DockPanel>
            <TextBlock Text="入力欄2-1"></TextBlock>
            <TextBox Width="100"></TextBox>
            <TextBlock Text="入力欄2-2"></TextBlock>
            <TextBox Width="100"></TextBox>
        </DockPanel>
    </Border>
    <DockPanel KeyboardNavigation.TabNavigation="Local" KeyboardNavigation.TabIndex="0">
        <Button Content="前へ"></Button>
        <Button Content="次へ"></Button>
    </DockPanel>
</DockPanel>

このように設定すると、Panelの子要素はPanel内で完結したタブインデックスを独自に持つようになるため、親パネル→子要素→親パネル→子要素という順で動くようになります。つまり次の順序です。

image.png

Panelにはタブフォーカスが当たらないため実際には2,3,5,6,8,9の順で動き、期待通りの動作になりました。

まとめ

Panelなどの親要素だけにタブオーダーを指定して並べ替えたいという要望を、「親は100番台、子は1番台」のようなハックをしなくても、親要素にだけ属性を指定することでシンプルに実現できました。WPFはやはりそれ以前のUIフレームワークから着実に進化していると思います。こういうちょっとしたテクニックの積み重ねは開発スピードや保守性に効いてくるので、積極的に使っていきたいですね。