24

是否有任何体面的方法来获得绑定到一个decimal值的 WPF 控件?

当我只是将TextBoxor绑定DataGridTextColumn到 adecimal时,数据输入是一个问题。

<TextBox Text="{Binding MyDecimal, UpdateSourceTrigger=PropertyChanged, 
    ValidatesOnDataErrors=True}"/>

当我尝试在其中输入“0,5”时,TextBox我会得到“5”。几乎不可能输入“0,5”(除了输入 1,5 并将“1”替换为“0”)。

当我使用StringFormat时,数据输入仅略有改进:

<TextBox Text="{Binding MyDecimal, StringFormat=F1, UpdateSourceTrigger=PropertyChanged,
    ValidatesOnDataErrors=True}"/>

现在,当我尝试输入“0,5”时,我最终会得到“0,5,0”,这仍然是错误的,但至少我可以毫不费力地删除尾随的“,0”。

尽管如此,decimal使用 WPF 输入类型还是很尴尬的,因为这些TextBoxes容易出现数据输入错误,尤其是对于值来说,这是一个真正的痛苦!

那么我应该在 WPF 中使用什么来输入十进制数据呢?或者微软不支持十进制数据?

4

14 回答 14

27

我目前将此行为用于数字和十进制输入:

public class TextBoxInputBehavior : Behavior<TextBox>
{
    const NumberStyles validNumberStyles = NumberStyles.AllowDecimalPoint |
                                               NumberStyles.AllowThousands |
                                               NumberStyles.AllowLeadingSign;
    public TextBoxInputBehavior()
    {
        this.InputMode = TextBoxInputMode.None;
        this.JustPositivDecimalInput = false;
    }

    public TextBoxInputMode InputMode { get; set; }


    public static readonly DependencyProperty JustPositivDecimalInputProperty =
     DependencyProperty.Register("JustPositivDecimalInput", typeof(bool),
     typeof(TextBoxInputBehavior), new FrameworkPropertyMetadata(false));

    public bool JustPositivDecimalInput
    {
        get { return (bool)GetValue(JustPositivDecimalInputProperty); }
        set { SetValue(JustPositivDecimalInputProperty, value); }
    }

    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.PreviewTextInput += AssociatedObjectPreviewTextInput;
        AssociatedObject.PreviewKeyDown += AssociatedObjectPreviewKeyDown;

        DataObject.AddPastingHandler(AssociatedObject, Pasting);

    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        AssociatedObject.PreviewTextInput -= AssociatedObjectPreviewTextInput;
        AssociatedObject.PreviewKeyDown -= AssociatedObjectPreviewKeyDown;

        DataObject.RemovePastingHandler(AssociatedObject, Pasting);
    }

    private void Pasting(object sender, DataObjectPastingEventArgs e)
    {
        if (e.DataObject.GetDataPresent(typeof(string)))
        {
            var pastedText = (string)e.DataObject.GetData(typeof(string));

            if (!this.IsValidInput(this.GetText(pastedText)))
            {
                System.Media.SystemSounds.Beep.Play();
                e.CancelCommand();
            }
        }
        else
        {
            System.Media.SystemSounds.Beep.Play();
            e.CancelCommand();
        }
     }

     private void AssociatedObjectPreviewKeyDown(object sender, KeyEventArgs e)
     {
        if (e.Key == Key.Space)
        {
            if (!this.IsValidInput(this.GetText(" ")))
            {
                System.Media.SystemSounds.Beep.Play();
                e.Handled = true;
            }
        }
     }

     private void AssociatedObjectPreviewTextInput(object sender, TextCompositionEventArgs e)
     {
        if (!this.IsValidInput(this.GetText(e.Text)))
        {
            System.Media.SystemSounds.Beep.Play();
            e.Handled = true;
        }
     }

     private string GetText(string input)
     {
        var txt = this.AssociatedObject;

        int selectionStart = txt.SelectionStart;
        if (txt.Text.Length < selectionStart) 
            selectionStart = txt.Text.Length;

        int selectionLength = txt.SelectionLength;
        if (txt.Text.Length < selectionStart + selectionLength) 
            selectionLength = txt.Text.Length - selectionStart;

        var realtext = txt.Text.Remove(selectionStart, selectionLength);

        int caretIndex = txt.CaretIndex;
        if (realtext.Length < caretIndex) 
            caretIndex = realtext.Length;

        var newtext = realtext.Insert(caretIndex, input);

        return newtext;
     }

     private bool IsValidInput(string input)
     {
        switch (InputMode)
        {
            case TextBoxInputMode.None:
                return true;
            case TextBoxInputMode.DigitInput:
                return CheckIsDigit(input);

            case TextBoxInputMode.DecimalInput:
                decimal d;
                //wen mehr als ein Komma
                if (input.ToCharArray().Where(x => x == ',').Count() > 1)
                    return false;


                if (input.Contains("-"))
                {
                     if (this.JustPositivDecimalInput) 
                        return false;


                     if (input.IndexOf("-",StringComparison.Ordinal) > 0) 
                          return false;

                      if(input.ToCharArray().Count(x=>x=='-') > 1)
                          return false;

                        //minus einmal am anfang zulässig
                       if (input.Length == 1) 
                           return true;
                    }

                    var result = decimal.TryParse(input, validNumberStyles, CultureInfo.CurrentCulture, out d);
                    return result;



            default: throw new ArgumentException("Unknown TextBoxInputMode");

        }
        return true;
     }

     private bool CheckIsDigit(string wert)
     {
        return wert.ToCharArray().All(Char.IsDigit);
     }
}

 public enum TextBoxInputMode
 {
  None,
  DecimalInput,
  DigitInput
  }

XAML 用法如下所示:

<TextBox Text="{Binding Sum}">
    <i:Interaction.Behaviors>
        <Behaviors:TextBoxInputBehavior InputMode="DecimalInput"/>
    </i:Interaction.Behaviors>
</TextBox>
于 2013-06-04T09:33:50.667 回答
12
    private void DecimalTextBox_PreviewTextInput(object sender, System.Windows.Input.TextCompositionEventArgs e)
    {
        bool approvedDecimalPoint = false;

        if (e.Text == ".")
        {
            if (!((TextBox)sender).Text.Contains("."))
                approvedDecimalPoint = true;
        }

        if (!(char.IsDigit(e.Text, e.Text.Length - 1) || approvedDecimalPoint))
            e.Handled = true;
    }
于 2015-04-08T17:49:04.613 回答
6

WPF 扩展工具包有一个DecimalUpDown控件,可能适合您的需要。它是免费使用的,最好使用它而不是尝试自己动手。

至于验证其上的输入,有多种应用验证的方法,这里是MSDN 中详细介绍的一种。我在我的博客上的两篇文章中详细介绍了自定义可绑定验证的另一种方法(您可以将验证应用于ValueDecimalUpDown 控件上的属性绑定)。

于 2013-06-04T09:32:30.723 回答
6

我也遇到过这个问题;UpdateSourceTrigger=PropertyChanged似乎绑定会在您键入文本时尝试更新文本。为了解决这个问题,我们更改了输入字段UpdateSourceTrigger=LostFocus,例如:

<TextBox Text="{Binding MyDecimal, UpdateSourceTrigger=LostFocus, ValidatesOnDataErrors=True, StringFormat=n1}" />

IDataErrorInfo您可以使用该界面定义自己的验证错误。您只需将以下内容添加到您的支持模型中:

 public class MyModel : IDataErrorInfo
 {
    /* my properties */

    public string Error { get { return null; } }
    public string this[string name]
    {
       get
       {
          switch (name)
          {
             case "MyDecimal":
                return NumberHelper.IsValidValue(MyDecimal) ? message : null;
             default: return null;
          }
       }
    }
    private string message = "Invalid value";
 }
于 2014-01-23T12:07:28.980 回答
4

我实现了自己的文本框。当文本中有数字时,它会更新源,否则不会。在失去焦点时,我阅读了源属性。您所要做的就是用此类替换 TextBox 并绑定 double 类型的“Number”属性。

public class DoubleTextBox: TextBox
{
    public DoubleTextBox()
    {
        TextChanged += DoubleTextBox_TextChanged;
        LostFocus += DoubleTextBox_LostFocus;
    }

    void DoubleTextBox_LostFocus(object sender, System.Windows.RoutedEventArgs e)
    {
        Text = Number.ToString("N2");
    }

    void DoubleTextBox_TextChanged(object sender, TextChangedEventArgs e)
    {
        double zahl;
        if (string.IsNullOrWhiteSpace(Text))
        {
            Number = 0;
        }
        else if (double.TryParse(Text, out zahl))
        {
            Number = Double.Parse(zahl.ToString("N2"));
        }
        else
        {
            ValidationError validationError =
                new ValidationError(new ExceptionValidationRule(), GetBindingExpression(NumberProperty));

            validationError.ErrorContent = "Keine gültige Zahl";

            Validation.MarkInvalid(
                GetBindingExpression(NumberProperty),
                validationError);

        }
    }

    public double Number
    {
        get { return (double)this.GetValue(NumberProperty); }
        set { this.SetValue(NumberProperty, value); }
    }

    public static readonly DependencyProperty NumberProperty = DependencyProperty.Register(
        "Number", typeof(double), typeof(DoubleTextBox), 
        new FrameworkPropertyMetadata
            (
                0d,
                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault
            )
    );
}
于 2014-11-01T21:30:49.180 回答
4

从 .NET 4.5 开始,有一个更简单的修复,将“延迟”添加到绑定

 <TextBox  Text="{Binding MyDouble, UpdateSourceTrigger=PropertyChanged, Delay=1000}" />

在绑定系统尝试替换句点(将“1.”更改为“1”)之前,用户现在有 1 秒(1000 毫秒)的时间。这应该让他们有时间在“。”之后输入其他字符。这样它就不会被删除。

于 2021-02-25T18:13:35.477 回答
3

如果您希望文本框只允许小数,则为该文本框编写 previewinputtext 事件。然后在那种情况下写下这段代码

decimal result;
e.Handled=!decimal.TryParse((sender as TextBox).Text + e.Text, out result)
于 2017-08-07T09:06:11.557 回答
3

我知道这篇文章很旧,但它首先出现在谷歌搜索上,以解决这个问题。由于 system.windows.interactivity 包(此包的旧版本)出现错误,我继续搜索。

MSDN上的这篇文章解决了我的问题,它是一个单行解决方案,就在主窗口上的初始化组件之前,如下所示:

    Public Sub New()

    ' This call is required by the designer.
    FrameworkCompatibilityPreferences.KeepTextBoxDisplaySynchronizedWithTextProperty = False
    InitializeComponent()

    ' Add any initialization after the InitializeComponent() call.

End Sub

希望这将有助于其他谷歌搜索者。

于 2019-07-09T21:12:34.767 回答
2

我是新手,所以我不能评论他的答案,但我修复了blindmeis代码中的负数问题。

只需修改

if (input.Contains("-"))

IsValidInput () 的部分...

                if (input.Contains("-"))
                {
                    if (this.JustPositivDecimalInput)
                        return false;

                    //minus einmal am anfang zulässig
                    //minus once at the beginning
                    if (input.IndexOf("-", StringComparison.Ordinal) == 0 && input.ToCharArray().Count(x => x == '-') == 1)
                    {
                        if(input.Length == 1)
                        {
                            //INPUT IS "-"
                            return true;
                        }
                        else if (input.Length == 2)
                        {
                            //VALIDATE NEGATIVE DECIMALS...INPUT IS "-."
                            if (input.IndexOf(".", StringComparison.Ordinal) == 1)
                            {
                                return true;
                            }
                        }
                        else 
                        {
                            return decimal.TryParse(input, validNumberStyles, CultureInfo.CurrentCulture, out d);
                        }
                    }
                }
于 2014-01-03T17:40:07.233 回答
2

这个正则表达式有效

private void TextBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
  {
   Regex regex = new Regex("^[.][0-9]+$|^[0-9]*[.]{0,1}[0-9]*$");
   e.Handled = !regex.IsMatch((sender as TextBox).Text.Insert((sender as TextBox).SelectionStart,e.Text));
  }
于 2018-01-17T17:51:01.843 回答
2

这将只允许在文本框中输入小数,而没有其他内容。

视图模型如下所示:

    private string _decimalVal = "0";
    public string decimalVal
    {
        get { return _decimalVal.ToString(); }
        set
        {
            if (string.IsNullOrEmpty(value) || value == "-")
                SetProperty(ref _decimalVal, value);
            else if (Decimal.TryParse(value, out decimal newVal))
            {
                if (newVal == 0)
                    value = "0";

                SetProperty(ref _decimalVal, value = (value.Contains(".")) ? Convert.ToDecimal(value).ToString("0.00") : value);
            }
        }
    }

XAML 用法如下所示:

<TextBox Text="{Binding decimalVal,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" />
于 2018-06-28T16:31:56.880 回答
0

我发现仅使用 PreviewTextInput 事件只会在您输入一些数字 1->12->123->-123(将光标移回)后输入负数时引起问题

在 PreviewTextInput 事件中移动插入符号这将不起作用(作为 TextBox 的发送者).Text + e.Text

使用以下内容获取正则表达式链接作为基本十进制数字正则表达式,其中小数点后的数字是可选的

确定 @"^[+-]?\d*.?\d*$" 这对我来说效果最好。

    string previousText = "";
    int previousCaretIndex = 0;
    private void txtB_PreviewTextInput(object sender, TextCompositionEventArgs e)
    {   
        previousText = ((System.Windows.Controls.TextBox)sender).Text;
        previousCaretIndex = ((System.Windows.Controls.TextBox)sender).CaretIndex;
    }

    private void txtB_TextChanged(object sender, TextChangedEventArgs e)
    {
        if(!Regex.IsMatch(((System.Windows.Controls.TextBox)sender).Text, @"^[+-]?\d*\.?\d*$"))
        {
            ((System.Windows.Controls.TextBox)sender).Text = previousText;
            ((System.Windows.Controls.TextBox)sender).CaretIndex = previousCaretIndex;
            e.Handled = true;
        }
    }
于 2020-04-20T01:32:35.043 回答
0

通过这种方法将防止将非整数和非十进制值复制和粘贴到TextBox我在其他任何答案中都看不到的值:

private void TextBox_PreviewTextInput(object sender, System.Windows.Input.TextCompositionEventArgs e)
{
    var textBoxText = ((System.Windows.Controls.TextBox)sender).Text;
    var regex = new System.Text.RegularExpressions.Regex(@"^\d+\.?\d*$");
    if (textBoxText.Length > 0)
    {
        textBoxText += e.Text;
        e.Handled = !regex.IsMatch(textBoxText);
    }
    else
    {
        e.Handled = !regex.IsMatch(e.Text);
    }
}

private void TextBox_PreviewExecuted(object sender, System.Windows.Input.ExecutedRoutedEventArgs e)
{
    if (e.Command == System.Windows.Input.ApplicationCommands.Paste)
    {
        if (System.Windows.Clipboard.ContainsText())
        {
            e.Handled = !new System.Text.RegularExpressions.Regex(@"^\d+\.?\d*$").IsMatch(System.Windows.Clipboard.GetText());
        }
    }
}

// In case user copies and pastes 2 times or more.
// E.G. "1.0" might give "1.01.0" and so on.
// E.G. if the regex expression is for the range of 1-100.
// Then user might delete some numbers from the input which would give "0" or "00" etc.
private void TextBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
{
    var textBox = (System.Windows.Controls.TextBox)sender;
    if (!new System.Text.RegularExpressions.Regex(@"^\d+\.?\d*$").IsMatch(textBox.Text.Trim()))
    {
        textBox.Clear();
    }
}

XAML:

<TextBox PreviewTextInput="TextBox_PreviewTextInput" CommandManager.PreviewExecuted="TextBox_PreviewExecuted" TextChanged="TextBox_TextChanged" HorizontalAlignment="Left" VerticalAlignment="Top" Width="120"/>

顺便说一句,如果您想更改其行为以接受其他模式(例如正则表达式),您只需将正则表达式更改为@"^\d+\.?\d*$"适合您需要的其他内容,这种方法似乎更简单可靠。

编辑

在某些情况下,取决于正则表达式,例如 HH:mm:ss 的日期时间正则表达式,其中TextChanged不接受 00: 之类的内容,因为您键入试图实现 00:20:00 时将停止在第三个数字 00:,因此,在这种情况下,如果您没有更好的正则表达式,则不要TextChanged使用以下命令:

private void TextBox_LostFocus(object sender, System.Windows.RoutedEventArgs e)
{
    var textBox = (System.Windows.Controls.TextBox)sender;
    var textBoxText = textBox.Text.Trim();
    if (textBoxText.Length == 0)
    {
        this.error = false; // It can be true too, depends on your logic.
    }
    else
    {
        this.error = !new System.Text.RegularExpressions.Regex(@"^\d+\.?\d*$").IsMatch(textBoxText);

        if (this.error)
        {
            textBox.Background = System.Windows.Media.Brushes.Red;
        }
        else
        {
            textBox.ClearValue(System.Windows.Controls.TextBox.BackgroundProperty);
        }
    }
}

error变量是一个成员变量,您应该使用它在表单末尾进行验证,例如通过单击按钮。

于 2021-05-29T04:12:18.127 回答
0

这是我部分基于其他答案的解决方案。控件“DoubleTextBox”包含可用于设置小数位数的属性“DecimalCount”。还处理了复制/粘贴、MVVM 和选择问题。它尚未经过全面测试,可能包含错误。如果是这样,我稍后会更新帖子。

XAML:

xmlns:local_validators="clr-namespace:YourApp.validators"
xmlns:local_converters="clr-namespace:YourApp.converters"

..

<local_controls:DoubleTextBox x:Name="tbPresetDose" DecimalCount="{Binding PresetDoseDecimalPointsCount}">
    <TextBox.Resources>
        <local_converters:DecimalPlaceStringFormatConverter x:Key="decimalPlaceStringFormatConverter"/>
    </TextBox.Resources>
    <TextBox.Text>
        <MultiBinding Converter="{StaticResource decimalPlaceStringFormatConverter}">
            <Binding Path="PresetDose"/>
            <Binding Path="PresetDoseDecimalPointsCount"/>
        </MultiBinding>
    </TextBox.Text>
</local_controls:DoubleTextBox>

双文本框控件:

public class DoubleTextBox : TextBox
{
    public DoubleTextBox()
    {
        DataObject.AddPastingHandler(this, OnPaste);
        PreviewTextInput += DoubleTextBoxPreviewTextInput;
    }
    private void OnPaste(object sender, DataObjectPastingEventArgs e)
    {
        if (e.DataObject.GetDataPresent(typeof(string)))
        {
            var pastedText = (string)e.DataObject.GetData(typeof(string));

            if (!IsValidInput(pastedText))
            {
                System.Media.SystemSounds.Beep.Play();
                e.CancelCommand();
            }
        }
        else
        {
            System.Media.SystemSounds.Beep.Play();
            e.CancelCommand();
        }
    }
    private void DoubleTextBoxPreviewTextInput(object sender, System.Windows.Input.TextCompositionEventArgs e)
    {
        String text;

        if (!String.IsNullOrEmpty(this.SelectedText))
        {
            text = this.Text.Remove(this.SelectionStart, this.SelectionLength);
            text = text.Insert(this.CaretIndex, e.Text);
        } 
        else
        {
            text = this.Text.Insert(this.CaretIndex, e.Text);
        }

        e.Handled = !IsValidInput(text);
    }
    public bool IsValidInput(string value)
    {
        if (String.IsNullOrEmpty(value))
            return false;

        string decimalNumberPattern = @"^[0-9]+(,[0-9]{0," + DecimalCount + @"})?$";
        var regex = new Regex(decimalNumberPattern);
        bool bResult = regex.IsMatch(value);
        return bResult;
    }
    public void DecimalCountChanged()
    {
        try
        {
            double doubleValue = double.Parse(Text, System.Globalization.CultureInfo.InvariantCulture);
            Text = doubleValue.ToString("N" + DecimalCount);
        }
        catch
        {
            Text = "";
        }
    }
    public double DecimalCount
    {
        get { return (double)this.GetValue(DecimalCountProperty); }
        set
        { 
            this.SetValue(DecimalCountProperty, value);
            DecimalCountChanged();
        }
    }
    public static readonly DependencyProperty DecimalCountProperty = DependencyProperty.Register(
        "DecimalCount", typeof(double), typeof(DoubleTextBox),
        new FrameworkPropertyMetadata
            (
                0d,
                FrameworkPropertyMetadataOptions.BindsTwoWayByDefault
            )
    );
}

DecimalPlaceStringFormatConverter:

public class DecimalPlaceStringFormatConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if (!decimal.TryParse(values[0].ToString(), out decimal value))
            return values[0].ToString();

        if (!int.TryParse(values[1].ToString(), out int decimalPlaces))
            return value;

        if (values.Length == 2)
            return string.Format($"{{0:F{decimalPlaces}}}", value);
        else
            return value;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        object dResult = DependencyProperty.UnsetValue;
        string strValue = value as string;
        double parcedDouble;
        if (double.TryParse(strValue, out parcedDouble))
        {
            dResult = parcedDouble;
        }

        return new object[] { dResult };
    }
}

视图模型:

private short _presetDoseDecimalPointsCount = 2;

..

public short PresetDoseDecimalPointsCount
{
    get => this._presetDoseDecimalPointsCount;
    set
    {
        if (value != _presetDoseDecimalPointsCount)
        {
            _presetDoseDecimalPointsCount = value;
            OnPropertyChanged();
        }
    }
}
于 2021-11-16T08:33:59.693 回答