はじめに
WPF カスタムコントールを用いることで、手軽に標準コントールの外観/挙動を変更することができます。
本記事では、ToggleButton 外観変更について記載します。
WPF カスタムコントール については下記記事もあります
テスト環境
ここに記載した情報/ソースコードは、Visual Studio Community 2022 を利用した下記プロジェクトで生成したモジュールを Windows 11 24H2 で動作確認しています。
- WPF - .NET Framework 4.8
- WPF - .NET 8
Visual Studio 2022 - .NET Framework 4.8 は、C# 7.3 が既定です。
このため、サンプルコードは、C# 7.3 機能範囲で記述しています。
ToggleButton
ToggleButton は、CheckBox、RadioButton といった状態を切り替えることができるコントロールの基本クラスです。
CheckBox 外観変更の記事で記載した IsThreeState プロパティは、そもそも ToggleButton で実装されているプロパティで、IsThreeState を true にすることで、 Indeterminate も使用可能となります。
状態 | IsChecked プロパティ値 |
---|---|
Checked(選択, ON) | true |
Unchecked(非選択, OFF) | false |
Indeterminate(不確定, Undefined) | null |
IsThreeState = false 時、ToggleButton クリック操作は、Checked, Unchecked トグル動作です。
IsThreeState = true 時、Checked, Indeterminate, Unchecked 循環トグル動作となります。
サンプル
初期デザイン
視認性などの観点で Switch / CheckBox が推奨されて、お目にかからなくなった Android の ToggleButton は、底辺にインジケータを配置して、TextOn / TextOff で指定された文字を ON / OFF で切り替える UI でした。
上記をベースとして、インジケータ表示位置を底辺から、左端にした UI を目指します。
インジケータエリア横幅を 20pixel とします。
インジケータ(indicator)は、コーナーラウンド有りの 1pixel 黒枠で、マージン(3,3,5,3)を設定して、Background は状態値によって配色を変更します。
ON時の表示(textOn)、OFF時の表示(textOff)を indicator 右に配置して、ON / OFF で表示を切り替えます。
IsThreeState="True" で利用される未定義時の表示(textUndefined)も同様に用意します。
indicator, textOn, textOff, textUndefined という名称を、後述 xaml サンプルコードで利用します。
最終デザイン
標準 ToggleButton と、後述サンプルコード CustomToggleButton の実行結果を掲載します。
上段が ToggleButton、下段が CustomToggleButton で、それぞれ「OFF」「ON」「ONでDIsable」を配置しています。
下段左は IsThreeState="True" としていて、「Undefined」時の表示を掲載しておきます。
サンプルプロジェクト
カスタムコントール追加
Visual Studio で WPF アプリケーションのプロジェクト WpfApp1 を作成して、ソリューションエクスプローラで Controls というサブフォルダを用意します。
この Controls というサブフォルダを選択して、追加
- 新しい項目
を選択します。
新しい項目追加で カスタムコントール(WPF)
を選択して、CustomToggleButton.cs
を作成します。
カスタムコントールを追加すると Theme\Generic.xaml
が自動的に追加されます。
namespace 修正
カスタムコントールを Controls 配下に配置したので、下記2ファイルの local namespece を修正します。
- Themes\Generic.xaml
- MainWindow.xaml
xmlns:local="clr-namespace:WpfApp1"
↓
xmlns:local="clr-namespace:WpfApp1.Controls"
サンプルコード
CustomToggleButton
CustomToggleButton 実装ポイントを記載します。
- DependencyProperty
- TextOff(OFF時の表示文字)TextOn(ON時の表示文字)を追加
- IsThreeState="True" で利用可能な TextUndefined(未定義時の表示文字)を追加
- コントール外観
- 全体を Border として、内部に Grid(横幅 20pixel、* )を配置
- Column="0" で「サンプル - 初期デザイン」に基づく Indicator を配置
- Column="1" に TextOff をバインドした textOff を Opacity="1" で配置
- Column="1" に TextOn をバインドした textOn を Opacity="0" で配置
- Column="1" に TextUndefined をバインドした textUndefined を Opacity="0" で配置
ControlTemplate.Triggers では下記を指定します。
- Checked 状態
- indicator.Background 配色変更
- textOff の Opacity を 0 として非表示
- textOn の Opacity を 1 として表示
- textUndefined の Opacity を 0 として非表示
- Indeterminate 状態
- indicator.Background 配色変更
- textOff の Opacity を 0 として非表示
- textOn の Opacity を 0 として非表示
- textUndefined の Opacity を 1 として表示
- Disable 状態
- indicator.Background, Background, BorderBrush, Foreground 配色変更
- MouseOver 状態
- Background 配色変更
特定の条件でのプロパティ変更として ControlTemplate.Triggers を利用しましたが、VisualStateManager を利用することもできます。
VisualStateManager は、より柔軟な状態管理を提供する仕組みで、特にアニメーションを伴う UI の変更に適しています。
<Style TargetType="{x:Type local:CustomToggleButton}"
BasedOn="{StaticResource {x:Type ToggleButton}}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:CustomToggleButton}">
<Border Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- Indicator -->
<Border x:Name="indicator" Grid.Column="0" Margin="3,3,5,3"
BorderBrush="Black" Background="Gainsboro"
BorderThickness="1" CornerRadius="2"/>
<!-- 表示文字:Off -->
<TextBlock x:Name="textOff" Opacity="1"
Grid.Column="1" Text="{TemplateBinding TextOff}"
HorizontalAlignment="Left" VerticalAlignment="Center"/>
<!-- 表示文字:On -->
<TextBlock x:Name="textOn" Opacity="0"
Grid.Column="1" Text="{TemplateBinding TextOn}"
HorizontalAlignment="Left" VerticalAlignment="Center"/>
<!-- 表示文字:Undefined -->
<TextBlock x:Name="textUndefined" Opacity="0"
Grid.Column="1" Text="{TemplateBinding TextUndefined}"
HorizontalAlignment="Left" VerticalAlignment="Center"/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<!-- Checked 状態 -->
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="indicator" Property="Background" Value="SkyBlue"/>
<Setter TargetName="textOff" Property="Opacity" Value="0"/>
<Setter TargetName="textOn" Property="Opacity" Value="1"/>
<Setter TargetName="textUndefined" Property="Opacity" Value="0"/>
</Trigger>
<!-- Indeterminate 状態 -->
<Trigger Property="IsChecked" Value="{x:Null}">
<Setter TargetName="indicator" Property="Background" Value="Salmon"/>
<Setter TargetName="textOff" Property="Opacity" Value="0"/>
<Setter TargetName="textOn" Property="Opacity" Value="0"/>
<Setter TargetName="textUndefined" Property="Opacity" Value="1"/>
</Trigger>
<!-- Disable 状態 -->
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="indicator" Property="Background" Value="White"/>
<Setter Property="Background" Value="GhostWhite" />
<Setter Property="BorderBrush" Value="Gray" />
<Setter Property="Foreground" Value="Gray"/>
</Trigger>
<!-- MouseOver 状態 -->
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="LightBlue" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
namespace WpfApp1.Controls
{
public class CustomToggleButton : ToggleButton
{
// Off 時の表示文字
public string TextOff
{
get { return (string)GetValue(TextOffProperty); }
set { SetValue(TextOffProperty, value); }
}
public static readonly DependencyProperty TextOffProperty =
DependencyProperty.Register("TextOff", typeof(string),
typeof(CustomToggleButton), new PropertyMetadata("Off"));
// On 時の表示文字
public string TextOn
{
get { return (string)GetValue(TextOnProperty); }
set { SetValue(TextOnProperty, value); }
}
public static readonly DependencyProperty TextOnProperty =
DependencyProperty.Register("TextOn", typeof(string),
typeof(CustomToggleButton), new PropertyMetadata("On"));
// Undefined 時の表示文字
public string TextUndefined
{
get { return (string)GetValue(TextUndefinedProperty); }
set { SetValue(TextUndefinedProperty, value); }
}
public static readonly DependencyProperty TextUndefinedProperty =
DependencyProperty.Register("TextUndefined", typeof(string),
typeof(CustomToggleButton), new PropertyMetadata("Undefined"));
// 静的コンストラクタ - クラス全体の初期化で1度だけ呼び出される
static CustomToggleButton()
{
// DefaultStyleKeyの設定
DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomToggleButton),
new FrameworkPropertyMetadata(typeof(CustomToggleButton)));
}
// インスタンス コンストラクタ - インスタンスごとに呼び出される
public CustomToggleButton()
{
}
}
}
メイン画面
メイン画面の xaml も掲載しておきます。
<Window x:Class="WpfApp1.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"
xmlns:local="clr-namespace:WpfApp1.Controls"
mc:Ignorable="d"
Title="MainWindow" Height="200" Width="600">
<StackPanel>
<Grid Height="120" Margin="2">
<Grid.RowDefinitions>
<RowDefinition Height="1*"/>
<RowDefinition Height="1*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<!-- 標準コントール -->
<ToggleButton x:Name="btnHoge00" Grid.Column="0" Grid.Row="0"
Width="150" Height="40"
HorizontalAlignment="Left" VerticalAlignment="Center"
Content="〇〇機能" IsThreeState="True"/>
<ToggleButton x:Name="btnHoge10" Grid.Column="1" Grid.Row="0"
Width="150" Height="40"
HorizontalAlignment="Left" VerticalAlignment="Center"
Content="△△機能" IsChecked="True"/>
<ToggleButton x:Name="btnHoge20" Grid.Column="2" Grid.Row="0"
Width="150" Height="40"
HorizontalAlignment="Left" VerticalAlignment="Center"
Content="□□機能" IsChecked="True" IsEnabled="False"/>
<!-- カスタムコントール -->
<local:CustomToggleButton x:Name="btnHoge01" Grid.Column="0" Grid.Row="1"
Width="150" Height="40"
HorizontalAlignment="Left" VerticalAlignment="Center"
TextOn="〇〇機能-標準" TextOff="〇〇機能-無効"
TextUndefined="〇〇機能-拡張" IsThreeState="True"
IsChecked="False"/>
<local:CustomToggleButton x:Name="btnHoge11" Grid.Column="1" Grid.Row="1"
Width="150" Height="40"
HorizontalAlignment="Left" VerticalAlignment="Center"
TextOn="△△機能-有効" TextOff="△△機能-無効"
IsChecked="True"/>
<local:CustomToggleButton x:Name="btnHoge21" Grid.Column="2" Grid.Row="1"
Width="150" Height="40"
HorizontalAlignment="Left" VerticalAlignment="Center"
TextOn="□□機能-有効" TextOff="□□機能-無効"
IsChecked="True" IsEnabled="False"/>
</Grid>
</StackPanel>
</Window>