WPF で子要素を持つ UserControl を作る
◆ はじめに
現在のプロジェクト案件で WPF を使っておりまして、レビューやらで Xaml をみる機会が増えてきました。その Xaml 内に同じような記述が繰り返しでてきたりすると、共通化や部品化したくなるのが人の性...。せっせこ部品化していたときに、子要素を持つコントロールってどうやって作るんだっけなーとふと思って調べたのでメモ。
◆ どんなコントロールを作るか
タイトル行付きの StackPanel を作ってみる。イメージは以下。タイトルとカラーはプロパティで指定できるようにする。
<Window x:Class="CustomStackPanel.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" Title="MainWindow" Width="450" Height="450" mc:Ignorable="d"> <Grid Margin="10"> <Grid.RowDefinitions> <RowDefinition Height="1*" /> </Grid.RowDefinitions> <!--ここから--> <Border Grid.Row="0" BorderBrush="SkyBlue" BorderThickness="3"> <StackPanel> <Label Content="グループ1" Foreground="SkyBlue" /> <Border Margin="5" BorderBrush="SkyBlue" BorderThickness="0,0,0,2" /> <StackPanel Margin="10"> <!--ここは可変--> <TextBlock x:Name="text1" Text="あいうえお" /> <TextBlock x:Name="text2" Text="あいうえお" /> <TextBlock x:Name="text3" Text="あいうえお" /> <TextBlock x:Name="text4" Text="あいうえお" /> <TextBlock x:Name="text5" Text="あいうえお" /> <!--ここは可変--> </StackPanel> </StackPanel> </Border> <!--ここまで部品化--> </Grid> </Window>
◆ 手順
1. UserControl を追加
プロジェクト -> ユーザコントロールの追加 で UserControl を新規作成する。Xaml ファイルとそれに紐づく C# のソースが生成される。UserControl 名は「GroupPanel」としている。
2. Xaml 修正
作りたいコントロールに合わせて、Xaml を修正していく。今回は子要素を持つ必要があるので、Template と ContentPresenter を使用している。以下を参考にした。
c# - Fill Stackpanel inside Usercontrol - Stack Overflow
Xaml はこんな感じになる。
GroupPanel.xaml
<UserControl x:Class="CustomStackPanel.GroupPanel" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Name="GroupPanelRoot"> <UserControl.Template> <ControlTemplate TargetType="UserControl"> <Border BorderBrush="{Binding StyleColor, ElementName=GroupPanelRoot}" BorderThickness="3"> <StackPanel> <Label Content="{Binding Title, ElementName=GroupPanelRoot}" Foreground="{Binding StyleColor, ElementName=GroupPanelRoot}" /> <Border Margin="5" BorderBrush="{Binding StyleColor, ElementName=GroupPanelRoot}" BorderThickness="0,0,0,2" /> <StackPanel Margin="10"> <ContentPresenter Content="{TemplateBinding Content}" /> </StackPanel> </StackPanel> </Border> </ControlTemplate> </UserControl.Template> </UserControl>
3. プロパティの追加
コントロールの見た目はできたので、あとは独自プロパティを追加する。独自プロパティは、追加した UserControl の C# 側に記載すれば OK。詳細は、依存関係プロパティとかでググれば出てくる。
GroupPanel.xaml.cs
using System.Windows; using System.Windows.Controls; using System.Windows.Media; namespace CustomStackPanel { /// <summary> /// GroupPanel.xaml の相互作用ロジック /// </summary> public partial class GroupPanel : UserControl { public static DependencyProperty StyleColorProperty = DependencyProperty.Register( "StyleColor", typeof(Brush), typeof(GroupPanel), new PropertyMetadata(new SolidColorBrush(Colors.Transparent))); public Brush StyleColor { get => (Brush)GetValue(StyleColorProperty); set { SetValue(StyleColorProperty, value); } } public static DependencyProperty TitleProperty = DependencyProperty.Register( "Title", typeof(string), typeof(GroupPanel), new PropertyMetadata("")); public string Title { get => (string)GetValue(TitleProperty); set { SetValue(TitleProperty, value); } } public GroupPanel() { InitializeComponent(); } } }
4. 子要素に Name 属性が持てない!
以上までで良し良し楽勝だなとか思っていたら、問題発生。作成した UserControl の子要素に Name 属性を持たせたらエラーが発生した。嘘だと言ってよバーニー。
こんな感じの Xaml をかくと、
<local:GroupPanel Title="グループ2" Grid.Row="1" StyleColor="IndianRed"> <StackPanel> <TextBlock x:Name="text1" Text="あいうえお" /> </StackPanel> </local:GroupPanel>
こんなエラーが出てくる。
重大度レベル コード 説明 プロジェクト ファイル 行 抑制状態 エラー XEC0030 名前 "text1" は既にこのスコープで定義されています。 CustomStackPanel MainWindow.xaml 41
なんでや。
5. 解決策
意味が分からなさ過ぎて、ネットの海をさまよっていたら、以下サイトたちに行き着いた。
How to create a WPF UserControl with Named content?
JD’s Blog » WPF: Cannot set Name attribute
記事によると、Xaml で画面作るとダメっぽいので、画面も C# 側で書け、以上。まじかよ。
Xaml なしの UserControl クラス(GroupPanel2) を作成してみたら、解決した。全然納得できないけど、しょうがないね。
GroupPanel2.cs
using System; using System.Windows; using System.Windows.Controls; using System.Windows.Media; namespace CustomStackPanel { public class GroupPanel2 : UserControl { public static DependencyProperty StyleColorProperty = DependencyProperty.Register( "StyleColor", typeof(Brush), typeof(GroupPanel2), new PropertyMetadata(new SolidColorBrush(Colors.Transparent))); public Brush StyleColor { get => (Brush)GetValue(StyleColorProperty); set { SetValue(StyleColorProperty, value); } } public static DependencyProperty TitleProperty = DependencyProperty.Register( "Title", typeof(string), typeof(GroupPanel2), new PropertyMetadata("")); public string Title { get => (string)GetValue(TitleProperty); set { SetValue(TitleProperty, value); } } protected override void OnInitialized(EventArgs e) { base.OnInitialized(e); Border parentBorder = new Border { BorderBrush = StyleColor, BorderThickness = new Thickness(3), }; StackPanel mainStack = new StackPanel(); Label titleLabel = new Label { Content = Title, Foreground = StyleColor, }; Border lineBorder = new Border { Margin = new Thickness(5), BorderBrush = StyleColor, BorderThickness = new Thickness(0, 0, 0, 2) }; StackPanel contentStack = new StackPanel { Margin = new Thickness(10) }; ContentPresenter contentP = new ContentPresenter { Content = Content }; contentStack.Children.Add(contentP); mainStack.Children.Add(titleLabel); mainStack.Children.Add(lineBorder); mainStack.Children.Add(contentStack); parentBorder.Child = mainStack; Content = parentBorder; } } }
6. その他
サンプルソースは以下。見たい方はどうぞ。
◆ まとめ
無理やり感あるけど、なんとか解決できて良かった。カスタムコントロール+スタイルで作る方がもしかしてスタンダードなやり方なのかもしれない。そっちのやり方も試したらのっける。