sh1’s diary

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

WPF カスタムコントロールの作り方(ToggleButton の例)

WPF のカスタムコントロールの作り方……というか、基本的には設計者が設計したとおりに動作すればそれでいいような気もしますが、WPF にはいろんなカスタムコントロールの作り方があって、お作法というのか一応、とりあえず基本の線路はこれってものがあると思っていて、そこからやりやすいようにすればいいと思います。

以下のリンクでは、ユーザーコントールで Toggle を実装しています。

記事中では WPF の ToggleButton は認識しづらい……という話でスタートしているので、 ToggleButton のカスタムコントロールを作る話になると思うのですが、UWP の ToggleSwitch に引っ張られたのかユーザーコントールで実装されています。

ユーザーコントールにすると、当然ですが UserControl のプロパティを持つことになるため、ToggleButton のプロパティをそのまま継承することができないです。IsChecked プロパティをそのまま使えないのは面倒ですよね。(記事を見ればわかりますが)Dependency Property を使って、ToggleButton っぽいプロパティの再定義をすることになります。私は、あまり効率のよいやり方だとは思わないのですが、0から丁寧に再設計をしたいなら良いのかもしれないです。

そんなところで、今回は、ToggleButton のカスタムコントロールで作るパターンを書いた記事です。

以前にも似た記事を書いていますが。

カスタムコントロールライブラリ

まず、カスタムコントロールを作成するソリューションのプロジェクトは、「WPF カスタム コントロール ライブラリ」に分けておくことをオススメします。理由は、カスタムコントロールを作るくらいだし、使い回せるようにしたほうがいいと思います。

WPF カスタム コントロール ライブラリ」を作成すると、プロジェクトの構成は、こんな形になっていると思います。

作成するカスタムコントロール.cs ファイルはこんな感じで作成されています。DefaultStyleKeyProperty はコントロールのデザインを決めるためのもので、Generic.xaml ファイルで設定されます。(これはそういう WPF のルールで、必ず Themes/Generic.xaml ファイルから読み込むことになっています)

デフォルトだと作成するカスタムコントロールのファイル名は CustomControl1 みたいになっていると思いますが、具体的な名前に変更します。今回は ToggleControl にします。本当は ToggleButton がいいけど、コントロールとしてすでに存在するためややこしいので。(あとで ToggleBoxToggleSwitch で良かったかもと思いました)

ToggleControl の継承元はデフォルトだと Control になっていると思います。具体的なコントールの継承予定があるなら、そっちにしたほうがいいです。たとえば、TextBox、CheckBox、ToggleButton のデザインを作り変えて、すこしプロパティを追加したい、くらいなら素直にそっちを継承します。今回の例では ToggleButton を作ります。なので、ControlToggleButton に書き換えておきます。

public class ToggleControl : Control // ToggleButton に書き換え
{
    static ToggleControl()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(ToggleControl), new FrameworkPropertyMetadata(typeof(ToggleControl)));
    }
}

Generic.xaml ファイルは、DefaultStyleKeyProperty で指定したデザインを記述してあります。プロジェクトが HeritageLibrary.Wpf.Controls だと、こんな感じに記述しています。

実際のところは Themes/ToggleControlTheme.xaml にコントールの具体的なスタイルを記述しています。BasedOn で継承してやることで、このファイルでもデザインを拡張できるメリットもありますが、どちらかというと Generic.xaml にごちゃごちゃとスタイルを記述すると、わけがわからないことになるので、このようにしています。

Generic.xaml の役割は、「あくまでもカスタムコントロールに、スタイルを適用すること」だけにしています。

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:heri_ctrl="clr-namespace:Heritage.Wpf.Control">
    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="/HeritageLibrary.Wpf.Controls;component/Themes/ToggleControlTheme.xaml" />
    </ResourceDictionary.MergedDictionaries>

    <Style TargetType="{x:Type heri_ctrl:ToggleControl}" BasedOn="{StaticResource ToggleControlDefaultStyle}" />

</ResourceDictionary>

ToggleControl のデザインはこんな感じです。このファイルでやっていることは ToggleControl のスタイルを定義しているだけです。それ以外はやりません。

これは、一般的なクラスファイルと同じ考えです。1つのクラス……ファイルに、いくつも役割を持たせると Git で更新履歴を管理するメリットが薄れてしまうと思います。ファイルの更新履歴を見たときに、どの役割に変更が入ったのか、パッと見てもはっきりしなくなってしまうと思います。1ファイルの役割が少ないほど、更新したときになぜ更新されたのかがわかりやすいです。

役割に変化がなければ、更新もされない。Generic.xaml も新しいコントロールを追加しない限りは更新されません。更新履歴の多いファイルは、作業/仕事をした感が出ているかもしれないですが、(個人的な意見ですが)そうしたファイルは、結構な割合で役割過多なファイルだと思っていて、設計的な意味で注意して見たりします。

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:hf_conv="clr-namespace:Heritage.Wpf.ValueConverters;assembly=HeritageLibrary.Wpf"
                    xmlns:hf_ctrl="clr-namespace:Heritage.Wpf.Control">

    <Style x:Key="ToggleControlDefaultStyle" TargetType="{x:Type hf_ctrl:ToggleControl}">

        <Setter Property="SnapsToDevicePixels" Value="True" />
        <Setter Property="UseLayoutRounding" Value="True" />
        <Setter Property="FocusVisualStyle" Value="{x:Null}" />
        <Setter Property="HorizontalAlignment" Value="Left" />
        <Setter Property="VerticalAlignment" Value="Top" />

        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type hf_ctrl:ToggleControl}">
                    <Border Name="_Box"
                                Height="20" Width="40"
                                CornerRadius="10"
                                BorderThickness="1" Padding="2">
                        <Border Name="_CheckMark" 
                                    Width="14"
                                    Height="14"
                                    HorizontalAlignment="Left"
                                    CornerRadius="10">
                            <ContentPresenter />
                        </Border>
                    </Border>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsChecked" Value="True">
                            <Setter TargetName="_CheckMark" Property="HorizontalAlignment" Value="Right" />
                            <Setter TargetName="_CheckMark" Property="Background" Value="#FFFFFFFF" />
                            <Setter TargetName="_CheckMark" Property="BorderBrush" Value="#FF0067C0" />
                            <Setter TargetName="_Box" Property="Background" Value="#FF0067C0" />
                            <Setter TargetName="_Box" Property="BorderBrush" Value="#FF0067C0" />
                            <Setter TargetName="_Box" Property="BorderThickness" Value="0" />
                        </Trigger>
                        <Trigger Property="IsChecked" Value="False">
                            <Setter TargetName="_CheckMark" Property="HorizontalAlignment" Value="Left" />
                            <Setter TargetName="_CheckMark" Property="Background" Value="#FF5B5B5C" />
                            <Setter TargetName="_Box" Property="Background" Value="Transparent" />
                            <Setter TargetName="_Box" Property="BorderBrush" Value="#FF868688" />
                            <Setter TargetName="_Box" Property="BorderThickness" Value="1" />
                        </Trigger>
                        <Trigger Property="IsEnabled" Value="False">
                            <Setter TargetName="_CheckMark" Property="Background" Value="#FFFFFFFF" />
                            <Setter TargetName="_CheckMark" Property="BorderBrush" Value="#FFCCCCCC" />
                            <Setter TargetName="_Box" Property="Background" Value="#FFCCCCCC" />
                            <Setter TargetName="_Box" Property="BorderBrush" Value="#FFCCCCCC" />
                            <Setter TargetName="_Box" Property="BorderThickness" Value="0" />
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>

    </Style>

</ResourceDictionary>

ToggleControl のうごき

こんな感じでテストしてみます。

<UserControl x:Class="..."
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:hf_ctrl="clr-namespace:Heritage.Wpf.Control;assembly=HeritageLibrary.Wpf.Controls"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <StackPanel Orientation="Vertical">
        <hf_ctrl:ToggleControl IsEnabled="False" />
        <hf_ctrl:ToggleControl />
    </StackPanel>
</UserControl>

ToggleButton の見た目が悪いだけなら、これだけでもかなり良くなったのではないかと思います。

個人的な意見として「カスタムコントロール」の欠点は、いつも作り方を忘れてしまっていることです。よく触る機能ではないので、触るたびに基本的な記憶が抜けてます。

そこで、もう少しデザインを凝るなら、ToggleButton の「xaml の実装」を確認するのも良いやり方だと思います。基本的なスタイルをカバーする方法を学ぶことができます。

サンプル

GitHub に、今回のサンプルを公開しています。

参考