スライダーの動きと連動して表示値が変更されるカスタムコントロールです。
なるべく簡単なコントロールの組み合わせを考えて見ましたが、カスタムコントロールを作る必要性があるかというと、正直微妙な感じです。
カスタムコントロールの動作確認といった感じです。
ソースコード
プロジェクトファイル
普通の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));
}
コメント