XAML: カスタムコントロール(今風なToggleButton)
いよいよもってカスタムコントロールである。ControlTemplate の知識等の集大成に近い。 なお、ほかにもいろいろな知識がいるので、結局こいつは難しい部類の印象。 そして、これをまとめるからには「ResourceDictionary」もまとめないといけない。 実のところ、外観だけなら「ResourceDictionary」のが適していると思う。
カスタムコントロールを使う場面は「独自のバインディングターゲットを作りたい」、これに尽きると思う。 このバインディングターゲットを作りたいというのは、「元のプロパティが依存関係プロパティじゃないから、カスタムコントロールで仕方なく依存関係プロパティにしなくちゃいけない」も含まれる。
独自バインディングターゲットはいらない、ってなれば、まぁResourceDictionaryでいいかなぁと個人的には思う。
目次
カスタムコントロールというのは、言ってしまえば自作コントロールである。
ユーザコントロールとちょっと似ているけれど、明確に違うところがある。 しれっと ResourceDictionary いるけどこれも入れておいた方がよさそうだったので入れた。
他にも差異がありそうな項目がいそうだが、とりあえずこれだけで。 見てのとおり、1点明確な違いはName属性が付与できるかできないかである。
Name属性については過去にまとめた。 backacheengineer.hatenablog.com したがって、Name属性をつけられるという点においてはカスタムコントロールはライブラリ向きということになる。
そのツール独自の画面でいい、となるとまぁユーザコントロールでよいだろう。
バインディングターゲットが必要ないのであれば、ユーザコントロールである必要もなく、ResourceDictionary を使えばよい。 デザインを作るのは困難だと思う。参考になるデザインは他のツールか、
Windowsの設定とかのデザインを見ればなんとなーくふわっと「こんなの作りたい」というのは出てくる。
けれども、ControlTemplate でそれを実現しようとなると普通に難しいという・・・。 そういう時には generic.xaml というファイルが下記フォルダにいるはずなので参考になる(はず)。
途中の番号部分はたぶんwin10のビルド番号かなんかなので、人によって違う。 C:\Program Files (x86)\Windows Kits\10\DesignTime\CommonConfiguration\Neutral\UAP\10.0.19041.0\Generic こいつには VisualStateManager とか ツールのテーマカラーとかがなんと定義されているのだ。
おーすごい。ただ量もえげつない。でも参考になる(はず)。 ということでカスタムコントロールで今風な ToggleButton を作ってみる。
普通の ToggleButton だと押してある状態がON、そうでないときOFFみたいなボタンなんだけれども、
今風だとなんかバーになってると思う。それを作る。 既に作ったのでまずは結果から。まだまだ改良の余地ありなやつだが、とりあえずできたんで・・・。
必要な機能は以下のとおり。実際に使う場合も含めて考えてる。 1 は TextBlock をそのまま使う。 テキストの切り替えは Trriger でサクッと。 2 はソースとなる変数を作って設定する。ターゲットは「Visibility」というenum。
これなら表示状態、非表示状態、非表示かつ描画領域非確保状態を扱える。 3 はソースとなる変数を作って設定する。ターゲットはもちろん Text である。 4 は内部コード内で初期化する。 5 は Slider を使って実装していく。 6 は Slider の外観を変更して対応する。 7 はどうしようか悩んだ結果、透明な ToggleButton をバーにかぶせることにした(おい)。
これでクリック動作は確保できるので、あとは VisualState を実装してやって、自分で動かす。 まずはXAMLから。
まとめていて気が付いたけど、これサイズが変更できないのでは・・・。とりあえずこのままで。
本当に改善の余地が多いなぁ。 コメントが一切ないのは、カスタムコントロールに対してコピペする可能性を考慮した。
日本語含むカスタムコントロールの「Generic.xaml」はビルドに失敗するんで。
(1文足せば日本語も使える。単純に「UTF-8」しかできないのが問題で、なんか「shift-jis」もいけるように1文だけ足せばいいはず) 続いてC#コード。
ここで注意すべきなのが、「バインディングソースは依存関係プロパティじゃなくていいこと」。
別に依存関係プロパティで定義してもいいんだけど、無駄なコードは避けておきたい。
継承元が ContentControl なので、IsChecked を実装している。
実は ToggleButton を継承すると IsChecked は独自実装する必要ないんだけど、VisualState の操作がうまくできなかった。
力不足だ。この継承をうまいこと使えれば、たぶんだが VisualState をもっと楽できる。 こんなもんだろうか。
ControlTemplate は本当に難しい。
今回の実装も、正直微妙だなと思っている。
うまいこと継承とか使いこなしたいところ。 あと、もっと今風にするなら丸くしたいなと。
Thumb コントロールの外観を丸くして、Rectangle の角を丸くすればもっと今風になるなって。
デザインは奥が深い。プログラマもデザインする時代となると、もっと本読まないといけないかもね。カスタムコントロール
機能
バインディングターゲット作成
Name属性付与
内部コード実装
外観の実装
カスタムコントロール
○
○
○
○
ユーザコントロール
○
×
○
○
ResourceDictionary
×
○
×
○
デザインを作るうえでの参考
今風な ToggleButton を作る
必要な機能
XAML・C#コード
<ControlTemplate x:Key="sliderTemplate" TargetType="Slider">
<Grid Width="70" Height="40">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Rectangle Name="checkedRect"
Grid.Column="2"
Fill="Red">
</Rectangle>
<Thumb Name="barThumb"
Grid.Column="1"
Width="24">
</Thumb>
<Rectangle Name="uncheckedRect"
Grid.Column="0"
Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Slider}, Path=Value}"
Fill="Blue"/>
</Grid>
</ControlTemplate>
<Style TargetType="{x:Type local:ModernToggle}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ModernToggle}">
<Grid HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
Margin="{TemplateBinding Margin}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CheckedState">
<VisualState x:Name="UnChecked">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="slider"
Storyboard.TargetProperty="Value"
To="0" Duration="0:0:0.2"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Checked">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="slider"
Storyboard.TargetProperty="Value"
To="46" Duration="0:0:0.2"/>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0"
Name="stateText"
Text="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},Path=UnCheckedStateText}"
FontSize="{TemplateBinding FontSize}"
Margin="24"
Visibility="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},Path=StateTextVisibility}"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
<Slider Grid.Column="1"
Name="slider"
Template="{StaticResource sliderTemplate}"
Value="0"
Maximum="46"/>
<ToggleButton Name="toggle"
IsChecked="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=IsChecked}"
Grid.Column="1"
Opacity="0"/>
</Grid>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding ElementName=toggle, Path=IsChecked}" Value="True">
<Setter TargetName="stateText" Property="Text" Value="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=CheckedStateText}"/>
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
public class ModernToggle : ContentControl
{
public event EventHandler CheckedChanged;
/// <summary>
/// text of checked state
/// </summary>
public string CheckedStateText { get; set; } = "ON";
/// <summary>
/// text of unchecked state
/// </summary>
public string UnCheckedStateText { get; set; } = "OFF";
/// <summary>
/// visibility statetext
/// </summary>
public Visibility StateTextVisibility { get; set; } = Visibility.Visible;
/// <summary>
/// dependencyproperty IsChecked
/// </summary>
public static DependencyProperty IsCheckedProperty { get; private set; }
/// <summary>
/// true: Checked, false: Unchecked
/// </summary>
public bool IsChecked
{
get => (bool)GetValue(IsCheckedProperty);
set => SetValue(IsCheckedProperty, value);
}
/// <summary>
/// static constructor
/// </summary>
static ModernToggle()
{
IsCheckedProperty = DependencyProperty.Register(nameof(IsChecked), typeof(bool), typeof(ModernToggle), new PropertyMetadata(false, OnCheckedChanged));
}
/// <summary>
/// event of CheckedChanged for DependencyProperty
/// </summary>
static void OnCheckedChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
(obj as ModernToggle).OnCheckedChanged(EventArgs.Empty);
}
/// <summary>
/// event of CheckedChanged
/// </summary>
protected virtual void OnCheckedChanged(EventArgs args)
{
VisualStateManager.GoToState(this, IsChecked == true ? "Checked" : "UnChecked", true);
CheckedChanged?.Invoke(this, args);
}
}
まとめ