前回
基礎練として、簡単なViewとViewModelの実装をしました。
最初に作った物をコピペしたので、XAMLが同じような内容(Grid周り)です。
記事を見直して、自分のことながら残念な気持ちに。
今回
そうならないために、カスタムコントロール/ユーザーコントロールを作って共通化するのが定石です。
カスタムコントロール/ユーザーコントロールの違い
私「違いは何ですか?要約して教えてください。」
AI「カスタムコントロール は 汎用性が高く、再利用しやすい 独立した UI 部品。テンプレートを自由にカスタマイズでき、複数のプロジェクトで利用可能。
ユーザーコントロール は 特定用途向け に既存のコントロールを組み合わせて作る UI 部品。再利用性は限定的で、そのプロジェクト内で使うことが多い。
違いは「汎用性」と「設計の自由度」! どちらを使うかは開発目的次第です。😃」
私「今回は、カスタムコントロールをやってみよう。」
先人の知恵に学ぶ
なんなら作りたくないし、あるもの利用するだけにしたいです。そんなもんです。
というわけで、こちら。
Windows Community Toolkit の SettingsCard
SettingsCard は、エクスペリエンスで設定を表示するために使用できるコントロールです。 これは、Windows 11 の既定のスタイルを使用し、使いやすく、すべてのアクセシビリティ標準を満たしており、設定ページの見栄えを良くします。
利用します。
Nugetします
XAMLだけ書き換えます
なんということでしょう!
あれだけ冗長だったXAMLがすっきり!!
Viva!!!
<Window
~略~
xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:controls="using:CommunityToolkit.WinUI.Controls">
~略~
<controls:SettingsCard
HeaderIcon="{ui:FontIcon Glyph=''}"
Header="ToggleSwitch & Change Theme"
Description="トグルスイッチでアプリケーションのテーマを編集します。">
<!-- 本体 -->
<ToggleSwitch x:Name="toggleSwitch" IsOn="{x:Bind ViewModel.ToggleSwitchValue, Mode=TwoWay}" Toggled="toggleSwitch_Toggled" OffContent="ライトモード" OnContent="ダークモード" />
</controls:SettingsCard>
実行
まさにWin11の設定画面のそれですね。
ちなみに、クリックしたら下部領域が開く奴は SettingExpander です。
SettingsCard のソースを調査
SettingsCardは、どうやって実現されているのか?ソースコードはこちら
よし、見た。
簡易版を実装してみる
SettingsCard は、機能が充実していて、ソースコードももりもりです。
ここは、シンプルにすることで理解が進むはず。
やりたいことは、
・ヘッダーアイコンを指定できる
・ヘッダーを指定できる
・説明文を指定できる
・コンテンツは、自由にカスタマイズ可能とする
です。
実装 (C#)
・アイコン・ヘッダー、説明文、コンテンツのDependencyPropertyを宣言する。
・それぞれのget;set;を実装する。
・カスタムコントロールの子要素として指定するのは"Content"であることを宣言する。(ContentProperty)
[ContentProperty(Name = "Content")]
public partial class SettingsCard : Control
{
public static DependencyProperty BuildDp<T, O>(string nameOf, T defaultValue, PropertyChangedCallback callback)
{
return DependencyProperty.Register(nameOf, typeof(T), typeof(O), new PropertyMetadata(defaultValue, callback));
}
public static DependencyProperty FontIconProperty =
BuildDp<object, SettingsCard>(nameof(HeaderIcon), new FontIcon(), static (d, e) => { });
public static DependencyProperty HeaderProperty =
BuildDp<string, SettingsCard>(nameof(Header), string.Empty, static (d, e) => { });
public static DependencyProperty DescriptionProperty =
BuildDp<string, SettingsCard>(nameof(Description), string.Empty, static (d, e) => { });
public static readonly DependencyProperty ChildrenProperty =
BuildDp<object, SettingsCard>(nameof(Content), new(), static (d, e) => { });
public SettingsCard() : base()
{
this.DefaultStyleKey = typeof(SettingsCard);
}
public object HeaderIcon
{
get => GetValue(FontIconProperty);
set => SetValue(FontIconProperty, value);
}
public string Header
{
get => (string)GetValue(HeaderProperty);
set => SetValue(HeaderProperty, value);
}
public string Description
{
get => (string)GetValue(DescriptionProperty);
set => SetValue(DescriptionProperty, value);
}
public object Content
{
get => GetValue(ChildrenProperty);
set => SetValue(ChildrenProperty, value);
}
}
テンプレート(ResourceDictionary)
<Style TargetType="local:SettingsCard">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:SettingsCard">
<!-- テンプレート全体 -->
<Grid x:Name="PART_Panel"
Padding="16" CornerRadius="4"
Background="{ThemeResource ControlFillColorDefaultBrush}"
HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- ヘッダーアイコン -->
<ContentPresenter
Grid.Column="0"
x:Name="PART_HeaderIcon" Content="{TemplateBinding HeaderIcon}"
Margin="6 6 12 6" />
<!-- ヘッダー・説明文 -->
<StackPanel
Grid.Column="1"
x:Name="PART_Header"
Orientation="Vertical" Spacing="4"
VerticalAlignment="Center" HorizontalAlignment="Stretch">
<TextBlock Text="{TemplateBinding Header}"
FontSize="14" TextWrapping="WrapWholeWords" />
<TextBlock Text="{TemplateBinding Description}"
FontSize="12" TextWrapping="WrapWholeWords" />
</StackPanel>
<!-- コンテンツ -->
<ContentPresenter Grid.Column="2"
x:Name="PART_Content" Content="{TemplateBinding Content}"
VerticalContentAlignment="Center"
HorizontalContentAlignment="Right" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
利用する(XAML)
<local:SettingsCard
HeaderIcon="{ui:FontIcon Glyph=''}"
Header="local:SettingsCard"
Description="SettingsCard風カスタムコントロール">
<!-- コンテンツ -->
<Grid>
<Button Content="Button" />
</Grid>
</local:SettingsCard>
実行結果
簡易版のSettingsCardが完成して、基本は理解できたと思います。
本来は、状態によって部品の表示/非表示、デザイン変更とかが必要なはず。
DependencyProperty定義するの楽にならないかな。
また、次回があればどうぞ。