XAMLで始めるWPF入門:カスタムコントロールによる表示機能付きスライダー

コンピュータ

スライダーの動きと連動して表示値が変更されるカスタムコントロールです。
なるべく簡単なコントロールの組み合わせを考えて見ましたが、カスタムコントロールを作る必要性があるかというと、正直微妙な感じです。
カスタムコントロールの動作確認といった感じです。

ソースコード

プロジェクトファイル

普通のWPFプロジェクト

ファイル名:xamlSliderWithTextblock.csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net8.0-windows</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
    <UseWPF>true</UseWPF>
  </PropertyGroup>

</Project>

カスタムコントロール

カスタムコントロールはControlを継承した、カスタムコントロールの振る舞いを定義するクラスと、構成するコントロールのレイアウトや見た目を定義するXAML(Generic.xaml)で構成されます。
NoXAMLの場合はC#のみでした。

ファイル名:Controls\SliderWithValue.cs

// Controls/SliderWithValue.cs
using System.Windows;
using System.Windows.Controls;

namespace XamlSample.Controls;

[TemplatePart(Name = PART_Slider, Type = typeof(Slider))]
public class SliderWithValue : Control
{
    private const string PART_Slider = "PART_Slider";
    private Slider? _slider;

    static SliderWithValue()
    {
        DefaultStyleKeyProperty.OverrideMetadata(
            typeof(SliderWithValue),
            new FrameworkPropertyMetadata(typeof(SliderWithValue)));
    }

    public static readonly DependencyProperty ValueProperty =
        DependencyProperty.Register(
            nameof(Value),
            typeof(double),
            typeof(SliderWithValue),
            new FrameworkPropertyMetadata(
                0.0,
                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
                OnValueChanged,
                CoerceValueInRange));

    public double Value
    {
        get => (double)GetValue(ValueProperty);
        set => SetValue(ValueProperty, value);
    }

    public static readonly DependencyProperty MinimumProperty =
        DependencyProperty.Register(nameof(Minimum), typeof(double),
            typeof(SliderWithValue), new PropertyMetadata(0.0, OnRangeChanged));
    public double Minimum
    {
        get => (double)GetValue(MinimumProperty);
        set => SetValue(MinimumProperty, value);
    }

    public static readonly DependencyProperty MaximumProperty =
        DependencyProperty.Register(nameof(Maximum), typeof(double),
            typeof(SliderWithValue), new PropertyMetadata(100.0, OnRangeChanged));
    public double Maximum
    {
        get => (double)GetValue(MaximumProperty);
        set => SetValue(MaximumProperty, value);
    }

    public static readonly DependencyProperty SliderWidthProperty =
        DependencyProperty.Register(nameof(SliderWidth), typeof(double),
            typeof(SliderWithValue), new PropertyMetadata(150.0));
    public double SliderWidth
    {
        get => (double)GetValue(SliderWidthProperty);
        set => SetValue(SliderWidthProperty, value);
    }

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();

        if (_slider != null)
            _slider.ValueChanged -= Slider_ValueChanged;

        _slider = GetTemplateChild(PART_Slider) as Slider;

        if (_slider != null)
        {
            _slider.Minimum = Minimum;
            _slider.Maximum = Maximum;
            _slider.Value   = (double)CoerceValueInRange(this, Value);
            _slider.ValueChanged += Slider_ValueChanged;
        }
    }

    private void Slider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
    {
        if (!Equals(Value, e.NewValue))
            Value = e.NewValue;
    }

    private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var c = (SliderWithValue)d;
        var coerced = (double)CoerceValueInRange(c, (double)e.NewValue);

        if (c._slider != null && c._slider.Value != coerced)
            c._slider.Value = coerced;
    }

    private static object CoerceValueInRange(DependencyObject d, object baseValue)
    {
        var c = (SliderWithValue)d;
        var v = (double)baseValue;
        if (v < c.Minimum) v = c.Minimum;
        if (v > c.Maximum) v = c.Maximum;
        return v;
    }

    private static void OnRangeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var c = (SliderWithValue)d;

        if (c._slider != null)
        {
            c._slider.Minimum = c.Minimum;
            c._slider.Maximum = c.Maximum;
        }

        // 範囲変更時に Value を再クランプ
        c.CoerceValue(ValueProperty);
    }
}

ファイル名:Themes\Generic.xaml
注)ディレクトリ名は「Themes」でないとうまく動作しませんでした。

<!-- Themes/Generic.xaml -->
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:XamlSample.Controls">

    <Style TargetType="{x:Type local:SliderWithValue}">
        <Setter Property="Focusable" Value="False"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:SliderWithValue}">
                    <StackPanel Orientation="Horizontal" Margin="10">
                        <Slider x:Name="PART_Slider"
                                VerticalAlignment="Center"
                                Width="{TemplateBinding SliderWidth}"
                                Minimum="{TemplateBinding Minimum}"
                                Maximum="{TemplateBinding Maximum}"
                                Value="{Binding Value, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"/>
                        <TextBlock x:Name="PART_Text"
                                   Width="50"
                                   Margin="10,0,0,0"
                                   VerticalAlignment="Center"
                                   Text="{Binding Value, RelativeSource={RelativeSource TemplatedParent}, StringFormat=F0}"/>
                    </StackPanel>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

カスタムコントロールを使う側のコード

<!-- MainWindow.xaml -->
<Window x:Class="XamlSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:controls="clr-namespace:XamlSample.Controls"
        Title="XAML Custom Control Sample" Width="420" Height="160">
    <Grid>
        <controls:SliderWithValue HorizontalAlignment="Center"
                                  VerticalAlignment="Center"
                                  Minimum="0" Maximum="100"
                                  SliderWidth="220"
                                  Value="{Binding Volume, Mode=TwoWay}"/>
    </Grid>
</Window>

ファイル名:MainWindow.xaml.cs

// MainWindow.xaml.cs
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;

namespace XamlSample;

public partial class MainWindow : Window, INotifyPropertyChanged
{
    private double _volume = 30;
    public double Volume
    {
        get => _volume;
        set { if (_volume != value) { _volume = value; OnPropertyChanged(); } }
    }

    public MainWindow()
    {
        InitializeComponent();
        DataContext = this;
    }

    public event PropertyChangedEventHandler? PropertyChanged;
    private void OnPropertyChanged([CallerMemberName] string? name = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}

実行例

コメント