WPF 上下ボタンで数値を増減するスピンボックスを作る

コンピュータ

標準コントロールでありそうなスピンボックスですが、WPFには存在しないようなので自作します。

まずは簡単にコードビハインドで作ります。


XAML

<Grid>
    <StackPanel
        HorizontalAlignment="Center"
        VerticalAlignment="Center"
        Orientation="Horizontal">
        <TextBox x:Name="ValueBox"
            FontSize="16"
            Width="60"
            Text="0"
            VerticalContentAlignment="Center"
            HorizontalContentAlignment="Right"/>

        <RepeatButton Content="▲" Click="Up_Click"/>
        <RepeatButton Content="▼" Click="Down_Click"/>
    </StackPanel>
</Grid>

コードビハインド

public partial class MainWindow : Window
{
    private int _value = 0;
    public MainWindow()
    {
        InitializeComponent();
    }
    private void Up_Click(object sender, RoutedEventArgs e)
    {
        _value++;
        ValueBox.Text = _value.ToString();
    }

    private void Down_Click(object sender, RoutedEventArgs e)
    {
        _value--;
        ValueBox.Text = _value.ToString();
    }
}

実行例

起動すると初期値は0

ボタンをおすと数値がカウントアップ(ダウン)します。


AttachedProperty化し、再利用しやすくします。

NumericSpinner.cs

using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;

namespace Maywork.WPF.Helpers;

public static class NumericSpinner
{
    // =========================
    // Enable
    // =========================
    public static readonly DependencyProperty EnableProperty =
        DependencyProperty.RegisterAttached(
            "Enable",
            typeof(bool),
            typeof(NumericSpinner),
            new PropertyMetadata(false, OnEnableChanged));

    public static void SetEnable(DependencyObject obj, bool value)
        => obj.SetValue(EnableProperty, value);

    public static bool GetEnable(DependencyObject obj)
        => (bool)obj.GetValue(EnableProperty);

    private static void OnEnableChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is not TextBox tb) return;

        if ((bool)e.NewValue)
        {
            tb.PreviewKeyDown += OnKeyDown;
            tb.PreviewMouseWheel += OnMouseWheel;
        }
        else
        {
            tb.PreviewKeyDown -= OnKeyDown;
            tb.PreviewMouseWheel -= OnMouseWheel;
        }
    }

    // =========================
    // ボタン連携
    // =========================
    public static readonly DependencyProperty TargetProperty =
        DependencyProperty.RegisterAttached(
            "Target",
            typeof(TextBox),
            typeof(NumericSpinner),
            new PropertyMetadata(null, OnTargetChanged));

    public static void SetTarget(DependencyObject obj, TextBox value)
        => obj.SetValue(TargetProperty, value);

    public static TextBox GetTarget(DependencyObject obj)
        => (TextBox)obj.GetValue(TargetProperty);

    private static void OnTargetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is not ButtonBase btn) return;

        btn.Click -= OnButtonClick;

        if (e.NewValue is TextBox)
        {
            btn.Click += OnButtonClick;
        }
    }

    private static void OnButtonClick(object sender, RoutedEventArgs e)
    {
        if (sender is not ButtonBase btn) return;

        var tb = GetTarget(btn);
        if (tb == null) return;

        int delta = btn.Content?.ToString()?.Contains("▼") == true ? -1 : +1;

        ChangeValue(tb, delta);
    }

    // =========================
    // 入力処理
    // =========================
    private static void OnKeyDown(object sender, KeyEventArgs e)
    {
        if (sender is not TextBox tb) return;

        if (e.Key == Key.Up)
        {
            ChangeValue(tb, +1);
            e.Handled = true;
        }
        else if (e.Key == Key.Down)
        {
            ChangeValue(tb, -1);
            e.Handled = true;
        }
    }

    private static void OnMouseWheel(object sender, MouseWheelEventArgs e)
    {
        if (sender is not TextBox tb) return;

        int delta = e.Delta > 0 ? +1 : -1;
        ChangeValue(tb, delta);
        e.Handled = true;
    }

    // =========================
    // コア
    // =========================
    private static void ChangeValue(TextBox tb, int delta)
    {
        if (!int.TryParse(tb.Text, out int value))
            value = 0;

        value += delta;

        tb.Text = value.ToString();
    }
}

スピンのボタンの文字が「▼」固定なのは御愛嬌。

コンテンツに画像などを使いたい場合は、

簡易的にTagプロパティを使う方法や

本格的にConditionalWeakTableで拡張する方法が

考えられます。


XAML

<Grid>
    <StackPanel
        HorizontalAlignment="Center"
        VerticalAlignment="Center"
        Orientation="Horizontal">
        <TextBox
            x:Name="ValueBox"
            FontSize="16"
            Width="60"
            Text="0"
            VerticalContentAlignment="Center"
            HorizontalContentAlignment="Right"
            h:NumericSpinner.Enable="True"/>

        <RepeatButton
            Content="▲"
            h:NumericSpinner.Target="{Binding ElementName=ValueBox}"/>
        <RepeatButton
            Content="▼"
            h:NumericSpinner.Target="{Binding ElementName=ValueBox}"/>
    </StackPanel>
</Grid>

利用側です。

NumericSpinner.csは結構コード量が多いですが、

ライブラリとして使い回せますので、

ユーザープログラムでは無いもとすると、

XAMLのみでスピンボックスが完結しています。

サンプルコードなので、Step、Min、Maxなど、あったほうが良さそうなプロパティが省かれています。


コメント