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
がいいけど、コントロールとしてすでに存在するためややこしいので。(あとで ToggleBox
や ToggleSwitch
で良かったかもと思いました)
ToggleControl の継承元はデフォルトだと Control
になっていると思います。具体的なコントールの継承予定があるなら、そっちにしたほうがいいです。たとえば、TextBox、CheckBox、ToggleButton のデザインを作り変えて、すこしプロパティを追加したい、くらいなら素直にそっちを継承します。今回の例では ToggleButton
を作ります。なので、Control
は ToggleButton
に書き換えておきます。
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 に、今回のサンプルを公開しています。