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 を使用している。以下を参考にした。
https://stackoverflow.com/questions/36352612/fill-stackpanel-inside-usercontrol
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. 解決策
意味が分からなさ過ぎて、ネットの海をさまよっていたら、以下サイトたちに行き着いた。
- https://social.msdn.microsoft.com/Forums/vstudio/en-US/7e106c07-be99-402a-bb2a-fa4637574b32/how-to-create-a-wpf-usercontrol-with-named-content?forum=wpf
- http://blog.bluecog.co.nz/archives/2007/08/27/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. その他
サンプルソースは以下。見たい方はどうぞ。
https://github.com/shuntaro4/WPFSampleCustomStackPanel
◆ まとめ
無理やり感あるけど、なんとか解決できて良かった。
カスタムコントロール+スタイルで作る方がもしかしてスタンダードなやり方なのかもしれない。
そっちのやり方も試したらのっける。