もくじ
https://tera1707.com/entry/2022/02/06/144447
やりたいこと
以前WPFでUserControlを作ったが、CustomControlというものもあるらしい。
どうも、UserCotnrolは、作ったときに定義した見た目(Template)をそのまま使うのだが、CustomControlは、それを使うときに、使う側がTemplateを定義して、見た目を好きにできるらしい。
それって、いつも使ってる、元からあるコントロールに対してTemplateを指定して見た目をカスタムするということが、自分で作ったコントロールでもできるということか。
私のお仕事上、いろんなコントロールを組み合わせて独自のコントロールを作るということは頻繁にあるが、それで作ったコントロールに対して、使う側で好きなテンプレートを指定したいということがなかったので、CustomControlを使う機会がなかった。
(コントロールのライブラリを作って、他に配る、とかするときに使うんだろなと思っていた)
が、とりあえず知ってて損はないと思ったのでいっぺん作ってみる。
前提
WinUI3 1.2.221109.1 VisualStudio2022
実験用プロジェクト作成と、カスタムコントロールの追加
まずは、VisualStudioでひな型をつくるところから。
追加 > 新しい項目、で、カスタムコントロール(WinUI3)を選ぶ。
Themesフォルダの中に、Generic.xamlが追加される。
また、指定した名前のcsファイルが追加される。
カスタムコントロール追加直後の初期状態
WinUI3のテンプレート作った直後
追加直後
追加直後のGeneric.xaml
→初期状態のコードは別にいらんので、後で消しちゃう。
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:CustomControlJikken2"> <Style TargetType="local:MySignal" > <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="local:MySignal"> <Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}"> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary>
追加直後のcsファイル(今回は「MySignal.cs」)
あとでここに必要なものを追記していく。
// Copyright (c) Microsoft Corporation and Contributors. // Licensed under the MIT License. using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Data; using Microsoft.UI.Xaml.Documents; using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Media; using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices.WindowsRuntime; // To learn more about WinUI, the WinUI project structure, // and more about our project templates, see: http://aka.ms/winui-project-info. namespace CustomControlJikken2 { public sealed class MySignal : Control { public MySignal() { this.DefaultStyleKey = typeof(MySignal); } } }
csのthis.DefaultStyleKey = typeof(MySignal);
は消してはいけない。
このコードで、デフォルトのテンプレートを取ってきてる。
これで、カスタムコントロールの追加はOK。次は、実験的に、中身を変えてみる。
カスタムコントロールの中身を自分の好きなようにする
実験アプリの仕様
今回は、実験的に、信号機のようなカスタムコントロールを作ってみた。仕様はこんなかんじ。
- 動き仕様
- ボタンを押すと、赤信号、青信号が一定間隔で交互に点灯する。
- 青信号、赤信号の点灯時間は、どちらも同じ。
- 点灯時間は、指定できる。
- 中身仕様
- クラス名は「MySignal」。
- 青信号用のGrid「PART_SignalGreen」と、
- 赤信号用のGrid「PART_SignalRed」を持っている。
- 信号点灯開始のためのボタン「PART_StartButton」を持っている。
- 点灯時間を設定するためのプロパティ「IntervalTime」が生えている。
実験コード
コードは、こんな感じ。
(https://github.com/tera1707/CustomControlJikken)
generic.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:CustomControlJikken2"> <Style TargetType="local:MySignal" > <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="local:MySignal"> <Grid Background="Gray" > <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <Grid x:Name="PART_SignalGreen" Grid.Row="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="5" BorderBrush="{TemplateBinding BorderBrush}" Background="Green" > <TextBlock Text="Go" HorizontalAlignment="Center" VerticalAlignment="Center"/> </Grid> <Grid x:Name="PART_SignalRed" Grid.Row="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="5" BorderBrush="{TemplateBinding BorderBrush}" Background="Red" > <TextBlock Text="stop" HorizontalAlignment="Center" VerticalAlignment="Center"/> </Grid> <Button x:Name="PART_StartButton" Grid.Row="2" Content="SignalStart" HorizontalAlignment="Center" VerticalAlignment="Center"/> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary>
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; using System; using Windows.UI; namespace CustomControlJikken2 { // usercontrolは、画面(xaml)とそのコードビハインド(cs)ががっちり紐づいてるが、 // CustomControlは、画面とコードががっちりは紐づいてない感じ?(最低限必要なものだけを、cs上で定義してる感じ。まただから、デフォルトstyleの中で名前を付けたコントロールがあっても、直接見えなくて、GetTemplateChildで探さないといけないっぽい。) // がっちり紐づいてないから、CustomControlを使う側で、styleとtemplateを指定できるのか。 // やるべきこと // xamlで、主要な部品に名前をつける // コードで、その主要な部品に対応する変数を定義する // コードで、ApplyTemplateの中で、GetTemplateChild()でその名前を付けた部品を探して変数に入れる // DependencyPropertyを作成して、このコントロールを使う側から設定、取得できるプロパティを生やす(UserControlと同じ) // あとは、その部品たちを使ってやりたいことをやる public sealed class MySignal : Control { /// <summary> /// 信号切り替わり間隔(簡単にするために、赤→青、青→赤 どちらも同じ時間で切り替わるようにした) /// </summary> public TimeSpan IntervalTime { get => (TimeSpan)GetValue(IntervalTimeProperty); set { SetValue(IntervalTimeProperty, value); } } public static readonly DependencyProperty IntervalTimeProperty = DependencyProperty.Register(nameof(IntervalTime), typeof(TimeSpan), typeof(MySignal), new PropertyMetadata(TimeSpan.FromSeconds(5))); //------------------------------------------------------ // 本コントロール中に必須で存在させたい部品(=generic.xamlに名前付きで作成したコントロール) private Grid signalGreen; private Grid signalRed; private Button startButton; private DispatcherTimer signalIntervalTimer = new DispatcherTimer(); // null:信号停止中 // false:赤信号表示中 true:青信号表示中 private bool? IsGoing = null; private static readonly SolidColorBrush GreenBrush = new(Color.FromArgb(0xff, 0, 0xff, 0)); private static readonly SolidColorBrush RedBrush = new(Color.FromArgb(0xff, 0xff, 0, 0)); private static readonly SolidColorBrush GrayBrush = new (Color.FromArgb(0xff, 0xaa, 0xaa, 0xaa)); public MySignal() { this.DefaultStyleKey = typeof(MySignal); signalIntervalTimer.Tick += SignalIntervalTimer_Tick; } private void SignalIntervalTimer_Tick(object sender, object e) { if (IsGoing == false) { // 青信号にする signalGreen.Background = GreenBrush; signalRed.Background = GrayBrush; IsGoing = true; } else { // 赤信号にする signalGreen.Background = GrayBrush; signalRed.Background = RedBrush; IsGoing = false; } } // 自分でoverrideを追加 // (クラス名のところでAlt+Enterを押して出たメニューで「上書きを生成」を選び、出てきた一覧からOnApplyTemplateを選ぶ、でもいい) protected override void OnApplyTemplate() { base.OnApplyTemplate(); if (startButton is not null) startButton.Click -= StartButton_Click; signalGreen = this.GetTemplateChild("PART_SignalGreen") as Grid; signalRed = this.GetTemplateChild("PART_SignalRed") as Grid; startButton = this.GetTemplateChild("PART_StartButton") as Button; if (startButton is not null) startButton.Click += StartButton_Click; } private void StartButton_Click(object sender, RoutedEventArgs e) { if (IsGoing == null) { signalIntervalTimer.Interval = IntervalTime; signalIntervalTimer.Start(); IsGoing = false; } else { signalIntervalTimer.Stop(); IsGoing = null; } // 一旦、全消灯にする signalGreen.Background = GrayBrush; signalRed.Background = GrayBrush; } } }
Custom Controlを作るうえで実装すること
generic.xamlにデフォルトテンプレートを書く
CustomControlは、UserControlと違って、CustomControlを使うときに、外からStyle/Templateを指定することができる。
つまり、機能はそのままで、使う側が好きな見た目にすることができるのが特徴。
だがまず作るうえでは、デフォルトのstyle(使う側がstyleを指定しなかったときに使うstyle)を作る。
今回だと、上の実験コードのgeneric.xamlがそれ。
デフォルトテンプレートの中の主要部品に名前を付ける
CustomControlは、使うときに外からTemplateを指定することができるが、その外からのTemplateの中にも、主要(必須)な部品は備わっている必要がある。
上の実験コードだと、「PART_*****」という名前の付いた部品がそれ。
CustomControlを作るうえでは、主要(必須)な部品には「PART_」というのを頭につけるのが慣例の様子。
コードに、主要な部品に対応する変数を定義し、関連付ける
デフォルトのテンプレートは見た目を司るが、コードの方が動きを司る。
そのコードとテンプレート、動きと見た目を関連付けるために、コード側に、テンプレート中の主要部品に対応する変数を定義し、PART_***と名付けた名前を使って紐づける。
上の実験コードだと、MySignal.cs中の、
private Grid signalGreen; private Grid signalRed; private Button startButton;
が変数で、
signalGreen = this.GetTemplateChild("PART_SignalGreen") as Grid; signalRed = this.GetTemplateChild("PART_SignalRed") as Grid; startButton = this.GetTemplateChild("PART_StartButton") as Button;
が、名前で紐づけている部分。
デフォルトテンプレート中のコントロールのイベントを関連付ける
デフォルトテンプレート中に配置したコントロールのイベント(「Click」など)を拾って何かする必要があれば、そのイベントに、カスタムコントロールのクラスのメソッドを紐づける。
上の実験コードで言うと、
protected override void OnApplyTemplate() { base.OnApplyTemplate(); if (startButton is not null) startButton.Click -= StartButton_Click; //★ココで古いイベントを解除し、 signalGreen = this.GetTemplateChild("PART_SignalGreen") as Grid; signalRed = this.GetTemplateChild("PART_SignalRed") as Grid; startButton = this.GetTemplateChild("PART_StartButton") as Button; //★ココでイベントを取りたいボタンのコントロールを取得して、 if (startButton is not null) startButton.Click += StartButton_Click; //★ココでイベントとクラス内のメソッドを紐づける }
の★を付けた3か所の部分。
古いイベントを解除するのは、styleを適用されるたびにOnApplyTemplate()が行われて、イベントが残りっぱなしだとリークしてしまうからと思われる。(styleを途中で付け替えることはあまりないかもしれないが。)
必要なプロパティを生やす
カスタムコントロールに必要なプロパティを作成する。
ここのやり方は、UserControlと同じ。→(UserControlの作り方)https://qiita.com/tera1707/items/13a9ee14c2936c068a45
実験コードだと、
public TimeSpan IntervalTime { get => (TimeSpan)GetValue(IntervalTimeProperty); set { SetValue(IntervalTimeProperty, value); } } public static readonly DependencyProperty IntervalTimeProperty = DependencyProperty.Register(nameof(IntervalTime), typeof(TimeSpan), typeof(MySignal), new PropertyMetadata(TimeSpan.FromSeconds(5)));
の部分。
これで、TimeSpan型の、初期値が5秒のプロパティが作成された。
あとは、用意したものを使ってやりたいことをコードに書く
今まで用意したデフォルトテンプレート、主要部品(PART_****)、イベント、プロパティを使って、やりたいことをコードに書けばOK。
今回の実験コードだと、ボタンを押したらタイマーをスタートして、タイマが満了するたびに青信号と赤信号を切り替えたい、そのタイマーが何秒で満了するかはプロパティの値を見て行いたい、ということなので、それを書いたのが上の実験コードとなる。
CustmoControlを使う側からテンプレートを入れてみる
ここまででCustomControlを作ってみたが、使うときに使う側が好きなテンプレートを指定できるのがCustomControlの特徴だったので、実際に使う側でテンプレートを指定して使ってみる。
MainWindow.xaml
<Window x:Class="CustomControlJikken2.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:CustomControlJikken2" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> <GridView> <!-- styleを指定しないと、Generic.xamlに書いた標準styleで動く --> <local:MySignal Width="200" Height="250" IntervalTime="0:0:1" /> <!-- Templateを指定すると、そのTemplateで動いてくれる --> <local:MySignal Width="200" Height="250" IntervalTime="0:0:3" > <local:MySignal.Style> <Style TargetType="local:MySignal"> <Setter Property="Template"> <Setter.Value> <!-- 丸い信号 --> <ControlTemplate> <Grid Background="Gray"> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <Button x:Name="PART_StartButton" Grid.Row="0" Content="Oshite Kudasai"/> <Grid x:Name="PART_SignalGreen" Grid.Row="1"> <Ellipse Fill="ForestGreen"/> </Grid> <Grid x:Name="PART_SignalRed" Grid.Row="2"> <Ellipse Fill="Pink"/> </Grid> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> </local:MySignal.Style> </local:MySignal> </GridView> </Window>
上に書いたのが、使う側でテンプレートをしてしないパターン。 下に書いたのが、使う側でテンプレートを指定するパターン。
動かしてみたのがこちら。
テンプレートを指定したほうの見た目がなんかちょっと思ったのと違うが、動きはしてくれてるのでよしとする。
終わり
とりあえず、動くCustomControlができた。
これだけでも何となくは動いているが、
見た目を制御するうえで、VisualStateMangaer
というのを使うと、
xamlのほうで、動きを表現できたりするらしい。
次回それをやってみる。
メモ
ソリューション直下の「Theme」フォルダの中の「generic.xaml」が特別で、カスタムコントロールはここからデフォルトのテンプレート(x:keyがなくて、ターゲットタイプが作成したカスタムコントロールのクラス名のTemplate)を探しに行くとのこと。
→それはそうなのだが、なんとなく、自前で作ったResourceDictionary.xamlの中にデフォルトテンプレートを書いててもうまく動く気がする。(まだ試してない2023/6/15)
参考
MS公式
https://learn.microsoft.com/en-us/windows/apps/winui/winui3/xaml-templated-controls-csharp-winui-3
MS公式 Custom XAML Controls(WPF)
https://learn.microsoft.com/en-us/archive/msdn-magazine/2019/may/xaml-custom-xaml-controls
かずきさんブログ カスタムコントロール
https://blog.okazuki.jp/entry/2014/09/08/221209
ユーザーコントロールの作り方(カスタムコントロールではなく、ゆーーざーコントロール)
https://qiita.com/tera1707/items/13a9ee14c2936c068a45
カスタムコントロールは、デフォルトスタイルがgeneric.xamlに書かれてしまう。csと分離されるのでなんかいやだなと思っていたら、下記のような強引な?やり方で対策できるらしい。こんなのありか?でもやってみる