sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

WPF 「リボン」コントロールを TabControl で作る

通常のリボンコントロール代替案は「Fluent.Ribbon」になるのですが、コントロールが Office っぽく寄せすぎてしまうため、とりあえずのリボンコントロールTabControl で作ってみた一例の記事です。

f:id:shikaku_sh:20200304134240g:plain:w500
シンプルなタブコントロール

こんな感じで、エクスプローラーくらいの見た目のリボンコントロールなら、それっぽく作ることができます。

使い方のコード

リボンメニューを作るときにポイントになるのは、左端の「ファイル」ボタンです。今回は ContentPresenter で対応してみました。RibbonTab(作成したコントロール)に用意したプロパティ Menu が対応しています。

Menu には System.Windows.Controls.Menu を設定してみると、こんな感じに。

Menu コントロールは、高さが自由ではないみたいです。固定値に設定すると子要素 MenuItem は縦方向の Center がうまく機能しなかったです。そこで仕方なく Width と Height を親要素にバインディングしてサイズを設定しています。(コントロールをテンプレートで作り直したほうがいいかも)

<local:RibbonTab>
    <local:RibbonTab.Menu>
        <Menu Background="{StaticResource RibbonMenu_DefaultColorKey}" Foreground="White" IsMainMenu="True" Width="60">
            <MenuItem Height="{Binding Path=ActualHeight, RelativeSource={RelativeSource FindAncestor,AncestorType={x:Type Menu}}}"
                        Width="{Binding Path=ActualWidth, RelativeSource={RelativeSource FindAncestor,AncestorType={x:Type Menu}}}"
                        HorizontalContentAlignment="Center"
                        Header="ファイル">
                <MenuItem Header="A" />
                <MenuItem Header="A" />
            </MenuItem>
        </Menu>
    </local:RibbonTab.Menu>
    <!-- タブの要素 -->
    <TabItem Header="ホーム">
        <StackPanel Orientation="Horizontal" MinHeight="112">
            <!-- 要素1 -->
            <DockPanel Width="100">
                <Button DockPanel.Dock="Top" Margin="0 20 0 0"
                        Template="{StaticResource RibbonButtonTemplate}"
                        HorizontalAlignment="Center">
                    <Button.Content>
                        <Border BorderThickness="1" BorderBrush="#FFCCCCCC" Padding="2">
                            <Image Width="40" Height="40" />
                        </Border>
                    </Button.Content>
                </Button>

                <TextBlock DockPanel.Dock="Bottom" Margin="0 10"
                            HorizontalAlignment="Center" VerticalAlignment="Bottom"
                            Text="サンプル" Foreground="#FFCCCCCC" />
            </DockPanel>
            <!-- 縦線 -->
            <Border Margin="0 6" BorderThickness="1 0 0 0" BorderBrush="#FFCCCCCC" />
        </StackPanel>
    </TabItem>
    <TabItem Header="共有">
        <StackPanel Orientation="Horizontal" MinHeight="112">
        </StackPanel>
    </TabItem>
    <TabItem Header="表示">
        <StackPanel Orientation="Horizontal" MinHeight="112">
        </StackPanel>
    </TabItem>
    <TabItem Header="クリップボード">
        <StackPanel Orientation="Horizontal" MinHeight="112">
        </StackPanel>
    </TabItem>
</local:RibbonTab>

リボン TabControl のコード

ポイントは TargetType で TabControl ではなくて自分で作成する TabControl を継承したクラス名を指定しているところ。

新しく追加した DependencyPropertyMenu を参照するためには、TabControl を対象にしていてもプロパティの存在がわからない。

<Style TargetType="{x:Type local:RibbonTab}">
    <Setter Property="BorderThickness" Value="1 1 1 1" />
    <Setter Property="BorderBrush" Value="{StaticResource RibbonBorderColorKey}" />
    <Setter Property="Padding" Value="0" />
    <Setter Property="FocusVisualStyle" Value="{x:Null}" />

    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:RibbonTab}">
                <Grid ClipToBounds="true" SnapsToDevicePixels="true" KeyboardNavigation.TabNavigation="Local">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition />
                        <ColumnDefinition Width="0" />
                    </Grid.ColumnDefinitions>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="*" />
                    </Grid.RowDefinitions>
                    <StackPanel Grid.Column="0" Grid.Row="0" Orientation="Horizontal">
                        <!-- ファイルボタン -->
                        <ContentPresenter Content="{TemplateBinding Menu}" 
                                            RecognizesAccessKey="True"
                                            Margin="0 0 0 0" 
                                            SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
                        <!-- 各タブボタン -->
                        <TabPanel IsItemsHost="true" Margin="2 2 2 0" 
                                    KeyboardNavigation.TabIndex="1" Panel.ZIndex="1" 
                                    SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
                    </StackPanel>

                    <Border Grid.Column="0" Grid.Row="1"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}"
                            Background="{TemplateBinding Background}"
                            KeyboardNavigation.DirectionalNavigation="Contained"
                            KeyboardNavigation.TabIndex="2"
                            KeyboardNavigation.TabNavigation="Local"
                            Panel.ZIndex="-1"
                            SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" >
                        <!-- 各タブの内容 -->
                        <ContentPresenter ContentSource="SelectedContent" 
                                            Margin="{TemplateBinding Padding}"
                                            SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
                    </Border>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>
<Style TargetType="{x:Type TabItem}">
    <Setter Property="FocusVisualStyle" Value="{x:Null}" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type TabItem}">
                <Border x:Name="ItemHeader"
                        BorderThickness="1 1 1 0"
                        Background="White"
                        Margin="-2 -2 0 -1">
                    <Border x:Name="ItemHeaderUnderBorder"
                            BorderThickness="0 0 0 1"
                            Margin="1 0 0 0">
                        <ContentPresenter x:Name="ContentPresenter"
                                            Margin="5 5 6 6" MinWidth="60" Height="18"
                                            HorizontalAlignment="Center" VerticalAlignment="Center"
                                            ContentSource="Header" >
                            <ContentPresenter.Resources>
                                <Style TargetType="{x:Type TextBlock}">
                                    <Setter Property="HorizontalAlignment" Value="Center" />
                                    <Setter Property="VerticalAlignment" Value="Center" />
                                </Style>
                            </ContentPresenter.Resources>
                        </ContentPresenter>
                    </Border>
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsSelected" Value="True">
                        <Setter TargetName="ItemHeader" Property="BorderThickness" Value="1 1 1 0" />
                        <Setter TargetName="ItemHeader" Property="BorderBrush" Value="{StaticResource RibbonBorderColorKey}" />
                        <Setter TargetName="ItemHeaderUnderBorder" Property="BorderBrush" Value="White" />
                        <Setter TargetName="ContentPresenter" Property="TextElement.Foreground" Value="{StaticResource RibbonTabItem_SelectedFontColorKey}" />
                    </Trigger>
                    <Trigger Property="IsSelected" Value="False">
                        <Setter TargetName="ItemHeader" Property="BorderBrush" Value="Transparent" />
                        <Setter TargetName="ItemHeader" Property="Background" Value="Transparent" />
                        <Setter TargetName="ItemHeaderUnderBorder" Property="BorderBrush" Value="{StaticResource RibbonBorderColorKey}" />
                        <Setter TargetName="ContentPresenter" Property="TextElement.Foreground" Value="{StaticResource RibbonTabItem_FontColorKey}" />
                    </Trigger>
                    <MultiTrigger>
                        <MultiTrigger.Conditions>
                            <Condition Property="IsMouseOver" SourceName="ItemHeader" Value="True" />
                            <Condition Property="IsSelected" Value="False" />
                        </MultiTrigger.Conditions>
                        <Setter TargetName="ContentPresenter" Property="TextElement.Foreground" Value="{StaticResource RibbonTabItem_SelectedFontColorKey}" />
                    </MultiTrigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

プロパティを追加したところ。

public partial class RibbonTab : TabControl
{
    #region DependencyProperties

    public static readonly DependencyProperty MenuProperty =
        DependencyProperty.Register
        (
            nameof(Menu),
            typeof(object),
            typeof(RibbonTab),
            new FrameworkPropertyMetadata(null)
        );

    /// <summary>
    /// 「ファイル」メニューを設定するプロパティを取得または設定します。
    /// </summary>
    [System.ComponentModel.Bindable(true)]
    public object Menu
    {
        get { return GetValue(MenuProperty); }
        set { SetValue(MenuProperty, value); }
    }

    #endregion

    public RibbonTab()
    {
        InitializeComponent();
    }
}

補足

WPF では、リボンコントロールを利用するためにかつて「Microsoft.Windows.Controls.Ribbon」というものが用意されたのですが、枠の太さが変わったり角が丸くなったりと、制御できないおかしな挙動をします。下記のタイトルのように、私もおすすめしません。

枠が細くなる問題は、Visual Studio のフェードバックにも出ていたと思いますが、コントロールは十分な修正がないままのようです。1

というか、コントロールデザイン自体が(リボンなのに)スッキリしておらず、古くなってしまっていると思います。

その他のトラブル

MenuMenuItem が左に開く、伸びていくことがあったら、レジストリーを修正して、再起動。

「1」だと問題があって、「0」に修正してください。(レジストリーなのでPC の再起動必須です)

HKEY_CURRENT_USER\Software\Microsoft\Windows NT\CurrentVersion\Windows

f:id:shikaku_sh:20200304134545p:plain:w500

f:id:shikaku_sh:20200304134606p:plain:w400

WACOM のペンタブなんかを利用していると発生してしまう別問題のようです。

サンプル

GitHub の「Samples」リポジトリーにまとめて公開しています。今回のサンプルは「RibbonMenuSample」です。

RibbonMenu コントロールは、途中で(時間切れ)投げ出したコンポーネントです。

参考

WPF 4.5入門

WPF 4.5入門


  1. feedback の URL が変わってしまって、どこにいったのか不明です。(旧 URL)