2012年3月23日金曜日

wpf : TabControlのタブの左右にパネルを配置

タブを扱うアプリケーションで、タブの左右にコントロールを置きたいケースは多々あります。 例えば、タブの操作をするボタンを付けたり、省スペースのためタブの右側にツールバーを置いたりです。

実際にそういうレイアウトのアプリケーションは多くありますが、wpfのデフォルトのTabControlではそういうふうにはできません。 ですがstyleを書けば簡単に実現可能です。 というわけで試しに作ってみました。 こんな見た目になります。

MainWindow全体にstyle設定をしたTabControlを載せています。 そしてデフォルトではタブしか表示されない場所にボタン1~4を追加してみました。 タブの左右にパネルを追加できるように改造し、左側にはDockPanelとボタン1を、右側にはStackPanelとボタン2~4を追加しています。

以下作り方です。 使い回しが効くようにカスタムコントロールとして作りました。 とりあえずVisual C#のソリューションエクスプローラから「追加→新しい項目→カスタムコントロール」とします。 TestTabControl.csとThemes\Generic.xamlができます。

使い回ししやすいようにGeneric.xamlを他のソースと同じフォルダに移してファイル名変更。 TestTabControl.xamlにしました。 ファイル名を変えると<ResourceDictionary Source="TestTabControl.xaml"/>とか名前空間の指定とかが面倒になってしまいますね。 でもそれはまぁ、実際に使うとき「使いまわし先のGeneric.xamlにTestTabControl.xamlの中身だけコピー&ペースト」とかでもいいんで、今はファイル名で分かるようにこうしました。

まずはTestTabControl.csの編集から。 TestTabControlクラスをTabControlの継承に書き換えて、依存プロパティとしてパネルを2つ追加します。

using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Markup;

namespace CustomTabControlTest
{
    public class TestTabControl : TabControl
    {
        static TestTabControl()
        {
            DefaultStyleKeyProperty.OverrideMetadata(
                    typeof(TestTabControl),
                    new FrameworkPropertyMetadata(typeof(TestTabControl))
            );
        }

        public static readonly DependencyProperty ToolPanelLeftProperty = DependencyProperty.Register(
                "ToolPanelLeft",
                typeof(Panel),
                typeof(TestTabControl)
        );

        public static readonly DependencyProperty ToolPanelRightProperty = DependencyProperty.Register(
                "ToolPanelRight",
                typeof(Panel),
                typeof(TestTabControl)
        );

        public Panel ToolPanelLeft
        {
            get
            {
                return (Panel)GetValue(ToolPanelLeftProperty);
            }
            set
            {
                SetValue(ToolPanelLeftProperty, value);
            }
        }

        public Panel ToolPanelRight
        {
            get
            {
                return (Panel)GetValue(ToolPanelRightProperty);
            }
            set
            {
                SetValue(ToolPanelRightProperty, value);
            }
        }
    }
}

2つのパネルを依存プロパティにすることで、TestTabControlを使う側はxamlの記述でパネルが追加できるようになります。

そしてTestTabControl.xamlはこうなりました。

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:CustomTabControlTest"
>
    <Style TargetType="{x:Type local:TestTabControl}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:TestTabControl}">
                    <Grid KeyboardNavigation.TabNavigation="Local">
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="*"/>
                        </Grid.RowDefinitions>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="Auto"/>
                            <ColumnDefinition Width="*"/>
                            <ColumnDefinition Width="Auto"/>
                        </Grid.ColumnDefinitions>
                        <ContentPresenter
                                Grid.Row="0"
                                Grid.Column="0"
                                Content="{TemplateBinding ToolPanelLeft}"
                                KeyboardNavigation.TabNavigation="Local"
                                KeyboardNavigation.DirectionalNavigation="Contained"
                                KeyboardNavigation.TabIndex="1"
                        />
                        <TabPanel
                                Grid.Row="0"
                                Grid.Column="1"
                                IsItemsHost="True"
                                Panel.ZIndex="1"
                                Margin="2, 0"
                                Background="Transparent"
                                VerticalAlignment="Bottom"
                                KeyboardNavigation.TabIndex="2"
                        />
                        <ContentPresenter
                                Grid.Row="0"
                                Grid.Column="2"
                                Content="{TemplateBinding ToolPanelRight}"
                                KeyboardNavigation.TabNavigation="Local"
                                KeyboardNavigation.DirectionalNavigation="Contained"
                                KeyboardNavigation.TabIndex="3"
                        />
                        <Border 
                                Grid.Row="1" 
                                Grid.Column="0"
                                Grid.ColumnSpan="3"
                                Background="#fafafa"
                                BorderBrush="Gray"
                                BorderThickness="1"
                                KeyboardNavigation.TabNavigation="Local"
                                KeyboardNavigation.DirectionalNavigation="Contained"
                                KeyboardNavigation.TabIndex="4"
                        >
                            <ContentPresenter ContentSource="SelectedContent"/>
                        </Border>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

Gridに並べるだけです。 TabPanelのところにタブが、<ContentPresenter ContentSource="SelectedContent"/>のところにタブに対応したコンテンツが表示されます。

TabPanelにIsItemsHost="True"を設定しないとタブが表示されないので注意。 アクティブタブの表示の関係で、TabPanelのマージンは↑の設定でギリギリです。 地味に忘れそうなのはVerticalAlignment="Bottom"の設定。 こうしないとタブが浮いてしまうのでこれも注意。

タブの表示位置(上下左右どこに付けるか?)を変えるときはレイアウトをちょっと変えればOKです。 具体的には次の項目を書き換えます。

  • GridのRowDefinitionsとColumnDefinitions
  • Gridの子の各コントロールのGrid.RowとGrid.Column
  • TabPanelのHorizontalAlignmentとVerticalAlignment

「TestTabControl.csにそういう依存プロパティを作ればいいんじゃないかなぁ」と思ったけど面倒なので割愛で。

ちなみに、TestTabControlの色とかスタイルの微調整とかは適当なので、コピー&ペーストで使いたいときは各自調整してください。

使う側のサンプルはこんな感じ。

<Window x:Class="CustomTabControlTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:app="clr-namespace:CustomTabControlTest"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <ResourceDictionary Source="TestTabControl.xaml"/>
    </Window.Resources>
    <app:TestTabControl>
        <app:TestTabControl.ToolPanelLeft>
            <Grid>
                <Button>ボタン1</Button>
            </Grid>
        </app:TestTabControl.ToolPanelLeft>
        <app:TestTabControl.ToolPanelRight>
            <StackPanel Orientation="Horizontal">
                <Button>ボタン2</Button>
                <Button>ボタン3</Button>
                <Button>ボタン4</Button>
            </StackPanel>
        </app:TestTabControl.ToolPanelRight>
        <TabItem Header="タブ1 あしか">
            <Label>タブ1 あしか</Label>
        </TabItem>
        <TabItem Header="タブ2 おっとせい">
            <Label>タブ2 おっとせい</Label>
        </TabItem>
        <TabItem Header="タブ3 あざらし">
            <Label>タブ3 あざらし</Label>
        </TabItem>
        <TabItem Header="タブ4 とど">
            <Label>タブ4 とど</Label>
        </TabItem>
    </app:TestTabControl>
</Window>

ToolPanelLeft、ToolPanelRightは省略可能です。

それにしても、パネルの追加でいちいちapp:TestTabControl.ToolPanelLeftって全部書かなくちゃならないのは面倒ですね。 ToolPanelLeftタグだけで済ますにはどうすればいいのでしょう?

まぁ、そこまで調べてたら時間がかかりそうなので今はこのままにしときますか。 気が向いたときに検索してそれっぽい情報があったら改めて考えるって事で。

「ToolPanelLeft上のボタンにイベントを追加する」とかは普通にできたので十分使えるっぽいです。 今はこれで満足。