WPF で依存関係プロパティ (Dependency Property) を含んだカスタムコントロールを作成する方法について、まとめた記事です。
- 1.カスタムコントロールのファイルを準備する
- 2.カスタムコントロールの cs ファイル
- 3.Generic.xaml の編集
- コントロールのスタイルを書く 専用リソースファイル
- 補足
- サンプル
- 参考
例えば、好みのデザインの TextBox
コントロールに、独自の Binding 可能なプロパティを追加する場合、特に XAML の書き方(デザイン)の問題で作り方はある程度決まっています。ポイントを整理しました。
1.カスタムコントロールのファイルを準備する
最初に新しい項目の追加から「カスタムコントロール」を追加します。「カスタムコントロール」を追加すると「Themes」フォルダとその中に「Generic.xaml」というリソースディクショナリーのファイルが追加されます。
Themes/Generic.xaml
というファイルは、(Unity にはよくあるのですが)特別な動作をするファイルです。ファイル名を変更したり、フォルダー位置を変更しないでください。1
これは特殊なファイルです。フレームワークがこのファイルを自動的にアプリのリソースにマージします。ここで定義するリソースは、アプリケーション レベルでスコープ設定されます。「Microsoft Docs - カスタム XAML コントロール」
カスタムコントロールで作成したファイル自体は、cs ファイル単体です。複数のコントロールの組み合わせる目的で主に利用されるユーザーコントロールに慣れていると、xaml ファイルと cs ファイルがセットになっていないため、自由度が失われたような感じがしますが、気にしなくてもよいと思います。
カスタムコントロールは、Generic.xaml にスタイルを定義して、cs ファイルに依存関係プロパティを定義するものだと思えば、いつもの2ファイル編集になり、ほとんど変わりないと思います。
2.カスタムコントロールの cs ファイル
カスタムコントロール.cs
ファイルを開いてみると、静的コンストラクターで DefaultStyleKey
依存関係プロパティが設定されています。
static コンストラクター() { DefaultStyleKeyProperty.OverrideMetadata(typeof(カスタムコントロール名), new FrameworkPropertyMetadata(typeof(カスタムコントロール名))); }
これが Generic.xaml と連携するポイントになっています。ただ、どこにも Generic.xaml をインポートすると書いていないし、スタイルのキーを明示的に指定しているわけでもないです。
論理的な繋がりが見えないため、とっつきにくいと感じるかもしれないですが、Xceed.Wpf.Toolkit などの、主なカスタムコントロールもこのやり方を採用しているため、やってみて慣れるしかないのかと。
確認のついでに、今回のサンプルではテキストボックスのカスタムコントロール(DataGridCustomTextBoxColumn)を作成することにして、依存関係プロパティ SubText
を追加してみます。
public class カスタムコントロール名 : TextBox { public static readonly DependencyProperty SubTextProperty = DependencyProperty.Register( nameof(SubText), typeof(string), typeof(カスタムコントロール名), new UIPropertyMetadata(null) ); public string SubText { get { return (string)GetValue(SubTextProperty); } set { SetValue(SubTextProperty, value); } } ... }
3.Generic.xaml の編集
Generic.xaml ファイルは、次のようなテンプレートが最初に定義されています。基本的には、これがカスタムコントロールのスタイル定義になります。
<Style TargetType="{x:Type local:カスタムコントロール名}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:カスタムコントロール名}"> <Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style>
今回ならテキストボックスを作成するため、「Microsoft MsDocs - TextBox のスタイルとテンプレート」なんかを参考にしてスタイルを好みのとおりに編集します。
個人的なやり方ですが、Generic.xaml にいくつもカスタムコントロールを定義すると一覧性が悪くなったりします。(コンバーターやブラシ(色)の関連性がわかりづらくなりやすいです)なので、Themes フォルダーにリソースディクショナリーファイルを追加して、そっちのファイルに具体的なカスタムコントロール定義を書きます。
Generic.xaml ファイルは、リソースディクショナリーファイルをインポートして、カスタムコントロールのスタイル定義を一行追加するだけにしています。「Xceed.Wpf.Toolkit」でも、似たような方法が採用されています。
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:DependencyCustomControlSample"> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="/DependencyCustomControlSample;component/Themes/DataGridTextBoxColumnStyle.xaml" /> </ResourceDictionary.MergedDictionaries> <Style TargetType="{x:Type local:DataGridCustomTextBoxColumn}" BasedOn="{StaticResource DataGridCustomTextBoxColumnDefaultStyle}" /> </ResourceDictionary>
コントロールのスタイルを書く 専用リソースファイル
1つのファイルにカスタムコントロールのスタイルを1つ定義するため、わかりやすい構成になります。
留意する必要があるのは、カスタムコントロールでは OverridesDefaultStyle
を設定してはいけないです。
コントロールで OverridesDefaultStyle を
true
に設定した場合は、テーマスタイルによって提供される既定のコントロールテンプレートが抑制されます。「Microsoft Docs - FrameworkElement.OverridesDefaultStyle プロパティ」
以下、サンプルとして作ってみたテキストボックスのカスタムコントロールです。新しく追加した依存関係プロパティも簡単に設定できています。
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:DependencyCustomControlSample" xmlns:local_conv="clr-namespace:DependencyCustomControlSample.ValueConverter"> <local_conv:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" /> <SolidColorBrush x:Key="DisabledControlLightColor">#FFF4F4F4</SolidColorBrush> <SolidColorBrush x:Key="DisabledFontLightColor">#FF6D6D6D</SolidColorBrush> <Style x:Key="DataGridCustomTextBoxColumnDefaultStyle" TargetType="{x:Type local:DataGridCustomTextBoxColumn}"> <!-- <Setter Property="OverridesDefaultStyle" Value="True" /> --> <Setter Property="SnapsToDevicePixels" Value="True" /> <Setter Property="KeyboardNavigation.TabNavigation" Value="None" /> <Setter Property="FocusVisualStyle" Value="{x:Null}" /> <Setter Property="MinWidth" Value="120" /> <Setter Property="MinHeight" Value="20" /> <Setter Property="AllowDrop" Value="true" /> <Setter Property="HorizontalAlignment" Value="Stretch" /> <Setter Property="HorizontalContentAlignment" Value="Left" /> <Setter Property="VerticalAlignment" Value="Stretch" /> <Setter Property="VerticalContentAlignment" Value="Center" /> <Setter Property="Margin" Value="-1" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:DataGridCustomTextBoxColumn}"> <Border x:Name="MainBorder" Padding="2" BorderThickness="1" BorderBrush="Transparent" Background="{TemplateBinding Background}"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <ScrollViewer x:Name="PART_ContentHost" Grid.Column="0" Margin="0" FontFamily="Courier New" FontSize="15" TextOptions.TextFormattingMode="Display" TextOptions.TextRenderingMode="ClearType"> </ScrollViewer> <TextBlock x:Name="SubTextBlock" Grid.Column="1" HorizontalAlignment="Left" VerticalAlignment="Bottom" Margin="0 0 1 1" Text="{TemplateBinding SubText}" FontSize="8" Foreground="#FF666666" Visibility="{TemplateBinding IsEnabled, Converter={StaticResource BoolToVisibilityConverter}}" /> </Grid> </Border> <ControlTemplate.Triggers> <Trigger Property="IsEnabled" Value="False"> <Setter TargetName="MainBorder" Property="Background" Value="{StaticResource DisabledControlLightColor}" /> <Setter TargetName="MainBorder" Property="BorderThickness" Value="0" /> <Setter Property="Foreground" Value="{StaticResource DisabledFontLightColor}" /> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary>
補足
カスタムコントロールではなくて、ユーザーコントロールを使って同じことをやる場合を考えてみます。
ユーザーコントロールの中は、メインで利用するテキストボックスと、SubText
を表示するサブのテキストボックスの2つのコントロール構成でしょうか。
<UserControl x:class="UserControlLikeCustomControl"> <StackPanel> <TextBox x:Name="Main" /> <TextBox x:Name="Sub" /> </StackPanel> </UserControl>
このコントロールは、テキストボックスのように利用するため、当然このような感じで利用します。
<local:UserControlLikeCustomControl Text="" SubText="">
UserControl
はたくさんの依存関係プロパティを作成しないと連携できないことがわかると思います。Text
も SubText
も UserControl
はどのコントロールのどのプロパティと連携しているのかわかりません。TextBox
を直接設定したカスタムコントロールと比べて、1つ1つの感じがでます。
<UserControl x:class="UserControlLikeCustomControl" x:Name="RootControl"> <StackPanel> <TextBox x:Name="Main" Text="{Binding Text, ElementName=RootControl}" /> <TextBox x:Name="Sub" Text="{Binding SubText, ElementName=RootControl}" /> </StackPanel> </UserControl>
また、テキストボックス用の 添付プロパティ
を利用しようと思ったときにも、型が一致せず利用できないといった問題も発生するのではないかと思います。
<local:UserControlLikeCustomControl Text="" SubText="" Foreground="Green" Background="Red" local:MaskingTextBoxBehavior.Mask="^[0-9]+$">
細かく設定しようとしたときに、
TextBox
クラスではないため、プロパティの関連づけに面倒が増えてしまったり、うまく使えなかったりする。
ユーザーコントロールは、1つのコントロールを表現するよりも UserControl
という、あくまでコントロールをまとめた動きをするクラスというのが基本なんだと思います。
逆に、カスタムコントロールは、すでにあるコントロールの拡張くらいの認識が基本で、ちょうどよい気がします。
サンプル
GitHub の「Samples」リポジトリーにまとめて公開しています。今回のサンプルは「DependencyCustomControlSample」です。
こんな感じで、いつでも単位が見えるコントロールを作成しました。テキストボックスを継承しているから
IsEnabled
などのプロパティとのバインディングも当然できる。
いちおう、サンプルデータは以下のような感じです。
public MainWindow() { InitializeComponent(); Records.Add(new SampleData { No = 1, Text = "ABCDEF", SubText = "ポイント", IsEnabled = false }); Records.Add(new SampleData { No = 2, Text = "67890", SubText = "枚", IsEnabled = true }); Records.Add(new SampleData { No = 3, Text = "100", SubText = "G", IsEnabled = false }); Records.Add(new SampleData { No = 4, Text = "2000", SubText = "G", IsEnabled = true }); DataContext = this; }
参考
- 第3回 XAMLコードから生成されるプログラム・コードを理解する XAML(2): WPF固有機能の基礎 (3/4)
- WPF toolkit
- WPF4.5入門 その54 「カスタムコントロール」
- Microsoft Docs - FrameworkElement.OverridesDefaultStyle プロパティ
プログラミングWPF C#編―デザイナとプログラマのためのアプリケーション開発の極意
- 作者:日向 俊二
- メディア: 単行本
リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)
- 作者:Dustin Boswell,Trevor Foucher
- 発売日: 2012/06/23
- メディア: 単行本(ソフトカバー)