0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

はじめに

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

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

テスト環境

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

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

記載したソースコードは .NET 8 ベースとしています。
.NET Framework 4.8 の場合は、コメントで記載している null 許容参照型の明示 ? を削除してください。

Visual Studio 2022 - .NET Framework 4.8 は、C# 7.3 が既定です。
このため、サンプルコードは、C# 7.3 機能範囲で記述しています。

CheckBox

CheckBox 状態値、通常は Checked / Unchecked の2パターンですが、IsThreeState プロパティを true にすることで、 Indeterminate も使用可能となります。

状態 IsChecked プロパティ値
Checked(選択) true
Unchecked(非選択) false
Indeterminate(不確定) null

IsThreeState = false 時、CheckBox クリック操作は、Checked, Unchecked トグル動作です。
IsThreeState = true 時、Checked, Indeterminate, Unchecked 循環トグル動作となります。

サンプル

初期デザイン

CheckBox カスタムコントールとして、下記外観を考えてみます。

CheckBox-01.png

横 19pixel 縦 20pixel(mainBody)を、レイアウトエリアとします。
枠部分(innerBorder)は 14 x 14 の正方形です。
Checked マーク(markChecked)は、実際に動作させて調整することにします。

mainBody, innerBorder, markChecked という名称を、後述 xaml サンプルコードで利用します。

最終デザイン

標準 CheckBox と、後述サンプルコード CustomCheckBox の実行結果を掲載します。
上段が CheckBox、下段が CustomCheckBox です。

CheckBox-02.png

サンプルプロジェクト

カスタムコントール追加

Visual Studio で WPF アプリケーションのプロジェクト WpfApp1 を作成して、ソリューションエクスプローラで Controls というサブフォルダを用意します。
この Controls というサブフォルダを選択して、追加 - 新しい項目 を選択します。

vs-01.png

新しい項目追加で カスタムコントール(WPF)を選択して、CustomCheckBox.cs を作成します。

vs-02-2.png

カスタムコントールを追加すると Theme\Generic.xaml が自動的に追加されます。

namespace 修正

カスタムコントールを Controls 配下に配置したので、下記2ファイルの local namespece を修正します。

  • Themes\Generic.xaml
  • MainWindow.xaml
xmlns:local="clr-namespace:WpfApp1"
 ↓
xmlns:local="clr-namespace:WpfApp1.Controls"

サンプルコード

CustomCheckBox

外観は、BulletDecorator を利用します。
このコントロールには Bullet と Child の2つのコンテンツプロパティがあります。

  • Bullet
    • CheckBox/RadioButtn 選択状態、箇条書きのマーカー/アイコン
  • Child
    • 選択肢内容、箇条書き内容

BulletDecorator.Bullet で CheckBox 選択状態の外観をデザインします。

  • レイアウト
    • 前述「サンプル - 初期デザイン」横 19pixel 縦 20pixel を Grid で定義
    • 縦方向
      • 最小 20pixel
      • Height="{TemplateBinding Height}" として、センタリングするため、最上下端 RowDefinition を Height="1*"(単純に Height="20" VerticalAlignment="Center" の場合、CustomCheckBox に Height 指定時センタリングされなかったため)
      • 上余白、正方形枠、下余白、それぞれの Height を 4, 14, 2 pixcel
    • 横方向
      • 19pxcel
      • 左余白、正方形枠、右余白、それぞれの Width を 1, 14, 4 pixcel
  • mainBody
    • レイアウト 行1列0 から3行3列のエリア
    • Pressed 状態で表示する Border を Transparent で指定
  • innerBorder
    • レイアウト 行2列1 から1行1列のエリア
    • 正方形の枠
  • markChecked
    • レイアウト 行1列0 から3行3列のエリア
    • Path で指定座標を折れ線で描画
    • Opacity="0" で透明
  • markIndeterminate
    • レイアウト 行2列1 から1行1列のエリア
    • 文字 X を中央配置
    • Opacity="0" で透明

BulletDecorator.Child で CheckBox 選択肢内容の外観をデザインします。

  • contentText
    • ContentPresenter だと、Foreground 指定ができないので、TextBlock 利用

ControlTemplate.Triggers では下記を指定します。

  • Checked 状態
    • markChecked の Opacity を 1 として表示
  • Indeterminate 状態
    • markIndeterminate の Opacity を 1 として表示
  • Press 状態
    • mainBody の Background を指定値として表示
  • Disable 状態
    • markChecked, contentText, innerBorder 配色変更

特定の条件でのプロパティ変更として ControlTemplate.Triggers を利用しましたが、VisualStateManager を利用することもできます。
VisualStateManager は、より柔軟な状態管理を提供する仕組みで、特にアニメーションを伴う UI の変更に適しています。

Generic.xaml
<!-- CustomCheckBox -->
<Style TargetType="{x:Type local:CustomCheckBox}"
       BasedOn="{StaticResource {x:Type CheckBox}}">
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="{x:Type local:CustomCheckBox}">
        <BulletDecorator>
          <BulletDecorator.Bullet>
            <Grid Height="{TemplateBinding Height}" MinHeight="20" Width="19">
              <Grid.RowDefinitions>
                <RowDefinition Height="1*"/>
                <RowDefinition Height="4"/>
                <RowDefinition Height="14"/>
                <RowDefinition Height="2"/>
                <RowDefinition Height="1*"/>
              </Grid.RowDefinitions>
              <Grid.ColumnDefinitions>
                <ColumnDefinition Width="1"/>
                <ColumnDefinition Width="14"/>
                <ColumnDefinition Width="4"/>
              </Grid.ColumnDefinitions>
              <!-- CheckBox レイアウトエリア -->
              <Border Name="mainBody"
                      Grid.Row="1" Grid.Column="0" Grid.RowSpan="3" Grid.ColumnSpan="3"
                      CornerRadius="4" BorderThickness="1"
                      BorderBrush="Transparent" Background="Transparent"/>
              <!-- CheckBox の Box  -->
              <Border Name="innerBorder"
                      Grid.Row="2" Grid.Column="1"
                      BorderThickness="1"
                      BorderBrush="Gray" Background="Transparent"/>
              <!-- Checked マーク -->
              <Path Name="markChecked"
                    Grid.Row="1" Grid.Column="0" Grid.RowSpan="3" Grid.ColumnSpan="3"
                    Opacity="0"
                    StrokeThickness="4" Stroke="SkyBlue">
                <Path.Data>
                  <PathGeometry>
                    <PathFigure IsClosed="False" StartPoint="4,10">
                      <LineSegment Point="9,14"/>
                      <LineSegment Point="18,1"/>
                    </PathFigure>
                  </PathGeometry>
                </Path.Data>
              </Path>
              <!-- Indeterminate マーク -->
              <TextBlock Name="markIndeterminate"
                         Grid.Row="2" Grid.Column="1"
                         Opacity="0" Margin="0,0,0,2"
                         HorizontalAlignment="Center" VerticalAlignment="Center"
                         Text="×"
                         FontSize="14" FontFamily="Meiryo UI"
                         Foreground="#808080" Focusable="False"/>
            </Grid>
          </BulletDecorator.Bullet>
          <BulletDecorator.Child>
            <TextBlock x:Name="contentText"
                       HorizontalAlignment="Left" VerticalAlignment="Center" 
                       Text="{TemplateBinding Content}"/>
          </BulletDecorator.Child>
        </BulletDecorator>

        <ControlTemplate.Triggers>
          <!-- Checked 状態 -->
          <Trigger Property="IsChecked" Value="True">
            <Setter TargetName="markChecked" Property="Opacity" Value="1"/>
          </Trigger>
          <!-- Indeterminate 状態 -->
          <Trigger Property="IsChecked" Value="{x:Null}">
            <Setter TargetName="markIndeterminate" Property="Opacity" Value="1"/>
          </Trigger>
          <!-- Pressed 状態 -->
          <Trigger Property="IsPressed" Value="True">
            <Setter TargetName="mainBody" Property="Background" Value="LightCyan"/>
          </Trigger>
          <!-- Disable 状態 -->
          <Trigger Property="IsEnabled" Value="False">
            <Setter TargetName="markChecked" Property="Stroke" Value="LightSlateGray"/>
            <Setter TargetName="contentText" Property="Foreground" Value="DimGray"/>
            <Setter TargetName="innerBorder" Property="Background" Value="Gainsboro"/>
            <Setter TargetName="innerBorder" Property="BorderBrush" Value="Silver"/>
          </Trigger>
        </ControlTemplate.Triggers>
      </ControlTemplate>
    </Setter.Value>
  </Setter>
</Style>
CustomCheckBox.cs
namespace WpfApp1.Controls
{
  public class CustomCheckBox : CheckBox
  {
    // 静的コンストラクタ - クラス全体の初期化で1度だけ呼び出される
    static CustomCheckBox()
    {
      // DefaultStyleKeyの設定
      DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomCheckBox), 
          new FrameworkPropertyMetadata(typeof(CustomCheckBox)));
    }
    // インスタンス コンストラクタ - インスタンスごとに呼び出される
    public CustomCheckBox()
    {

    }
  }
}

メイン画面

メイン画面の 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">
      <Grid.RowDefinitions>
        <RowDefinition Height="1*"/>
        <RowDefinition Height="1*"/>
      </Grid.RowDefinitions>
      <Grid.ColumnDefinitions>
      <ColumnDefinition Width="1*"/>
        <ColumnDefinition Width="1*"/>
        <ColumnDefinition Width="1*"/>
      </Grid.ColumnDefinitions>
      <!-- 標準コントール  -->
      <CheckBox x:Name="cbHoge00" Grid.Column="0" Grid.Row="0"
                IsThreeState="True" Content="選択状態"
                HorizontalAlignment="Left" VerticalAlignment="Center"
                IsChecked="True"/>
      <CheckBox x:Name="cbHoge10" Grid.Column="1" Grid.Row="0"
                IsThreeState="True" Content="不確定状態"
                HorizontalAlignment="Left" VerticalAlignment="Center"
                IsChecked="{x:Null}"/>
      <CheckBox x:Name="cbHoge20" Grid.Column="2" Grid.Row="0"
                IsThreeState="True" Content="無効状態"
                HorizontalAlignment="Left" VerticalAlignment="Center"
                IsChecked="True" IsEnabled="False"/>
      <!-- カスタムコントール  -->
      <local:CustomCheckBox x:Name="cbHoge01" Grid.Column="0" Grid.Row="1"
          IsThreeState="True" Content="選択状態"
          HorizontalAlignment="Left" VerticalAlignment="Center"
          IsChecked="True"/>
      <local:CustomCheckBox x:Name="cbHoge11" Grid.Column="1" Grid.Row="1"
          IsThreeState="True" Content="不確定状態"
          HorizontalAlignment="Left" VerticalAlignment="Center"
          IsChecked="{x:Null}"/>
      <local:CustomCheckBox x:Name="cbHoge21" Grid.Column="2" Grid.Row="1"
          IsThreeState="True" Content="無効状態"
          HorizontalAlignment="Left" VerticalAlignment="Center"
          IsChecked="True" IsEnabled="False"/>
    </Grid>
  </StackPanel>
</Window>
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?