自定义字符串格式文本输入wpf

custom string format text input wpf

我的值采用 [double-type-value][unit] 格式,其中单位可以是 "g" 或 "mg"(g 表示克,mg 表示毫克)。有没有办法允许用户仅以该格式在 TextBox 中输入文本。例如,就像只接受数字的迷你文本框和普通文本框或其他内容中值为 "g" 或 "mg" 的迷你组合框?在文本框中键入内容之前,单位的默认值为 "g" 会很好,这样如果有更多文本框,用户不必每次都在文本框末尾键入 g 或 mg。

编辑 我正在使用 MVVM 模式,因此隐藏代码违反了它。

要防止用户输入除数字以外的任何内容,您必须使用 PrevieTextInput 事件,为此创建自定义控件是有意义的。下面的几行将阻止用户输入数字以外的任何内容

    <Grid>            
        <TextBox Text="{Binding Text}" PreviewTextInput="TextBox_PreviewTextInput"/>
        <TextBlock HorizontalAlignment="Right" Margin="5,0">g</TextBlock>
    </Grid>


    private void TextBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
    {
        var tb = sender as TextBox;
        e.Handled = !double.TryParse(tb.Text+e.Text, out double d);
    }

P.S。如果你不喜欢使用 try Catch,你可以使用正则表达式

您实际上应该处理三个​​事件:

  • PreviewTextInput
  • PreviewKeyDown - 防止输入空白字符,因为 PreviewTextInput
  • 不处理它们
  • DataObject.Pasting 附加事件以防止用户从剪贴板粘贴无效文本

最好将此逻辑封装在一个行为中。 A 有类似行为的示例:TextBoxIntegerInputBehavior, TextBoxDoubleInputBehavior.

您可以对 TextBox 上的事件 PreviewTextInputDataObject.PastingPreviewKeyDown 使用正则表达式来检查新字符串是否匹配 regex,如果没有,您可以取消操作。

像这样:

xaml:

...
<TextBox PreviewTextInput="txtbox_PreviewTextInput" DataObject.Pasting="txtbox_Pasting" PreviewKeyDown="txtbox_PreviewKeyDown" />
...

后面的代码:

public partial class MainWindow : Window
{
    private Regex gramOrMilliGramRegex = new Regex("^[0-9.-]+(m?g)?$");

    public MainWindow ()
    {
        InitializeComponent();
    }

    private void txtbox_PreviewTextInput(object sender, TextCompositionEventArgs e)
    {
        if(sender is TextBox txtbox)
        {
            string newString = txtbox.Text.Substring(0, txtbox.CaretIndex) + e.Text + txtbox.Text.Substring(txtbox.CaretIndex); //Build the new string
            e.Handled = !gramOrMilliGramRegex.IsMatch(e.Text); //Check if it matches the regex
        }

    }

    private void txtbox_Pasting(object sender, DataObjectPastingEventArgs e)
    {
        if(sender is TextBox txtbox)
        {
            string newString = txtbox.Text.Substring(0, txtbox.CaretIndex) + e.DataObject.GetData(typeof(string)) as string + txtbox.Text.Substring(txtbox.CaretIndex); //Build new string
            if (!digitOnlyRegex.IsMatch(newString)) //Check if it matches the regex
            {
                e.CancelCommand();
            }
        }

    private void txtbox_PreviewKeyDown(object sender, KeyEventArgs e)
    {
        //Prevents whitespace
        if (e.Key == Key.Space)
        {
            e.Handled = true;
        }
        base.OnPreviewKeyDown(e);
    }
}


更新:正如您现在提到的,您正在使用 MVVM 并且不想违反该模式。

您需要将这些事件路由到 ViewModel 中的命令,并将事件放在上面。

您可以在 xaml 中的 TextBox 中使用此代码:

xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:cmd ="http://www.galasoft.ch/mvvmlight"

...

<TextBox>
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="PreviewTextInput">
            <cmd:EventToCommand Command="{Binding Mode=OneWay, Path=PreviewTextInputCommand}" PassEventArgsToCommand="True" />
        </i:EventTrigger>
        <i:EventTrigger EventName="DataObject.Pasting">
            <cmd:EventToCommand Command="{Binding Mode=OneWay, Path=DataObject_PastingCommand}" PassEventArgsToCommand="True" />
        </i:EventTrigger>
        <i:EventTrigger EventName="PreviewKeyDown">
            <cmd:EventToCommand Command="{Binding Mode=OneWay, Path=PreviewKeyDownCommand}" PassEventArgsToCommand="True" />
        </i:EventTrigger>
    </i:Interaction.Triggers>
</TextBox>

由于此输入的性质,我建议您创建一个小 CustomControl,更具体的 TextBox,它能够限制 Input 并转换 Text 到相应的值 -> a GramTextBox.

GramTextBox有一个DependencyProperty叫做Gram,代表输入的Text的值,可以绑定到一个ViewModel(注意:由于 GramTextBox 尝试更新绑定 Source).

,绑定必须包含 Mode=TwoWay

代码

public sealed class GramTextBox : TextBox
{
    //Constructor
    public GramTextBox() : base()
    {
        Text = "0g"; //Initial value
        TextChanged += OnTextChanged;
        DataObject.AddPastingHandler(this, OnPaste);
    }

    //Style override (get the Style of a TextBox for the GramTextBox)
    static GramTextBox()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(GramTextBox), new FrameworkPropertyMetadata(typeof(TextBox)));
    }

    //Define a DependencyProperty to make it bindable (dont forget 'Mode=TwoWay' due the bound value is updated from this GramTextBox)
    [Category("Common"), Description("Converted double value from the entered Text in gram")]
    [Browsable(true)]
    [Bindable(true)]
    public double Gram
    {
        get { return (double)GetValue(PathDataProperty); }
        set { SetCurrentValue(PathDataProperty, value); }
    }
    public static DependencyProperty PathDataProperty = DependencyProperty.Register("Gram", typeof(double), typeof(GramTextBox), new PropertyMetadata(0d));

    //Extract the Gram value when Text has changed
    private void OnTextChanged(object sender, TextChangedEventArgs e)
    {
        ExtractGram(Text);
    }

    //Suppress space input
    protected override void OnPreviewKeyDown(KeyEventArgs e)
    {
        e.Handled = e.Key == Key.Space;
    }

    //Check text inputs
    protected override void OnPreviewTextInput(TextCompositionEventArgs e)
    {
        e.Handled = !IsValidText(Text.Insert(CaretIndex, e.Text));
    }

    //check paste inputs
    private void OnPaste(object sender, DataObjectPastingEventArgs e)
    {
        //Check if pasted object is string
        if(e.SourceDataObject.GetData(typeof(string)) is string text)
        {
            //Check if combined string is valid
           if(!IsValidText(Text.Insert(CaretIndex, text))) { e.CancelCommand(); }
        }
        else { e.CancelCommand(); }
    }

    //Check valid format for extraction (supports incomplete inputs like 0.m -> 0g)
    private bool IsValidText(string text)
    {
        return Regex.IsMatch(text, @"^([0-9]*?\.?[0-9]*?m?g?)$");
    }

    //Extract value from entered string
    private void ExtractGram(string text)
    {
        //trim all unwanted characters (only allow 0-9 dots and m or g)
        text = Regex.Replace(text, @"[^0-9\.mg]", String.Empty);
        //Expected Format -> random numbers, dots and couple m/g

        //trim all text after the letter g 
        text = text.Split('g')[0];
        //Expected Format -> random numbers, dots and m

        //trim double dots (only one dot is allowed)
        text = Regex.Replace(text, @"(?<=\..*)(\.)", String.Empty);
        //Expected Format -> random numbers with one or more dots and m

        //Check if m is at the end of the string to indicate milli (g was trimmed earlier)
        bool isMilli = text.EndsWith("m");

        //Remove all m, then only a double number should remain
        text = text.Replace("m", String.Empty);
        //Expected Format -> random numbers with possible dot

        //trim all leading zeros
        text = text.TrimStart(new char[] { '0' });
        //Expected Format -> random numbers with possible dot

        //Check if dot is at the beginning
        if (text.StartsWith(".")) { text = $"0{text}"; }
        //Expected Format -> random numbers with possible dot

        //Check if dot is at the end
        if (text.EndsWith(".")) { text = $"{text}0"; }
        //Expected Format -> random numbers with possible dot

        //Try to convert the remaining String to a Number, if it fails -> 0
        Double.TryParse(text, out double result);

        //Update Gram Property (divide when necessary)
        Gram = (isMilli) ? result / 1000d : result;
    }
}

用法

将此 Class 放入 YOURNAMESPACE 并在 XAML 中添加命名空间别名

xmlns:cc="clr-namespace:YOURNAMESPACE"

现在GramTextBox可以这样使用

<cc:GramTextBox Gram="{Binding VMDoubleProperty, Mode=TwoWay}" ... />

它会在每次 GramTextBoxText 发生变化时更新 ViewModel 中的绑定 Property(例如来自 keyboard/paste 等的有效输入) .

备注

旨在将 .00g0.0m.mg 等无意义的输入设置 Gram Property0(如回退值)。

个人笔记

感谢@Pavel PasteHandler

编辑

要在 DataGrid 中使用此 GramTextBox,您可以覆盖 ColumnCellTemplate

<DataGrid AutoGenerateColumns="False" ... >
    <DataGrid.Columns>
       <!-- Put some other Columns here like DataGridTextColumn -->
       <DataGridTemplateColumn Header="Mass">
           <DataGridTemplateColumn.CellTemplate>
               <DataTemplate>
                   <cc:GramTextBox Gram="{Binding VMDoubleProperty, Mode=TwoWay}" ... />
               </DataTemplate>
           </DataGridTemplateColumn.CellTemplate>
       </DataGridTemplateColumn>
       <!-- Put some other Columns here -->
   </DataGrid.Columns>
</DataGrid>