☯️

WPF - カスタムコントール - ToggleButton 外観変更

に公開

はじめに

WPF カスタムコントールを用いることで、手軽に標準コントールの外観/挙動を変更することができます。
本記事では、ToggleButton 外観変更について記載します。

WPF カスタムコントール については下記記事もあります

テスト環境

ここに記載した情報/ソースコードは、Visual Studio Community 2022 を利用した下記プロジェクトで生成したモジュールを Windows 11 24H2 で動作確認しています。

  • WPF - .NET Framework 4.8
  • WPF - .NET 8

ToggleButton

https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.controls.primitives.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 でした。

https://techbooster.org/android/ui/9445/

上記をベースとして、インジケータ表示位置を底辺から、左端にした UI を目指します。

インジケータエリア横幅を 20pixel とします。
インジケータ(indicator)は、コーナーラウンド有りの 1pixel 黒枠で、マージン(3,3,5,3)を設定して、Background は状態値によって配色を変更します。
ON時の表示(textOn)、OFF時の表示(textOff)を indicator 右に配置して、ON / OFF で表示を切り替えます。
IsThreeState="True" で利用される未定義時の表示(textUndefined)も同様に用意します。

最終デザイン

標準 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 配色変更
Generic.xml
<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>
CustomToggleButton.cs
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 も掲載しておきます。

MainWindow.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>

出典

本記事は、2025/05/12 Qiita 投稿記事の転載です。

WPF - カスタムコントール - ToggleButton 外観変更

Discussion