自定义字符串格式文本输入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
上的事件 PreviewTextInput
、DataObject.Pasting
和 PreviewKeyDown
使用正则表达式来检查新字符串是否匹配 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}" ... />
它会在每次 GramTextBox
的 Text
发生变化时更新 ViewModel
中的绑定 Property
(例如来自 keyboard/paste 等的有效输入) .
备注
旨在将 .00g
、0.0m
、.mg
等无意义的输入设置 Gram
Property
为 0
(如回退值)。
个人笔记
感谢@Pavel PasteHandler
编辑
要在 DataGrid
中使用此 GramTextBox
,您可以覆盖 Column
的 CellTemplate
:
<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>
我的值采用 [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
上的事件 PreviewTextInput
、DataObject.Pasting
和 PreviewKeyDown
使用正则表达式来检查新字符串是否匹配 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}" ... />
它会在每次 GramTextBox
的 Text
发生变化时更新 ViewModel
中的绑定 Property
(例如来自 keyboard/paste 等的有效输入) .
备注
旨在将 .00g
、0.0m
、.mg
等无意义的输入设置 Gram
Property
为 0
(如回退值)。
个人笔记
感谢@Pavel PasteHandler
编辑
要在 DataGrid
中使用此 GramTextBox
,您可以覆盖 Column
的 CellTemplate
:
<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>