WPF 如何创建具有验证和绑定的自定义文本框
WPF How to create a Custom Textbox with validation and binding
我正在开发用于货币编辑的自定义文本框。
我见过一些现成可用的,但它们很复杂 and/or 并不能真正使用,迫使您采取不良做法(例如硬编码应该在控件上使用的名称)。
所以我决定自己做,但是我在使用绑定选项时遇到了麻烦,因为分配给绑定属性的 属性 必须是小数,但是 Text 属性 TextBox 控件接受字符串。
我想的答案是,也许,将访问方法(getter 和 setter)覆盖到 base class(TextBox)中的 Text 属性,但这是不允许的。
我的绑定应设置为值,即设置 TextBox 的文本 属性 随时随地将其格式化为文本(带有货币符号和所有内容),但在 Get 方法中将其转换回数字数据类型。
这是我到目前为止所取得的成就:
public class CurrencyTextBox : TextBox
{
private bool IsValidKey(Key key)
{
int k = (int)key;
return ((k >= 34 && k <= 43) //digits 0 to 9
|| (k >= 74 && k <= 83) //numeric keypad 0 to 9
|| (k == 2) //back space
|| (k == 32) //delete
);
}
private void Format()
{
//formatting decimal to currency text here
//Done! no problems here
}
private void FormatBack()
{
//formatting currency text to decimal here
//Done! no problems here
}
private void ValueChanged(object sender, TextChangedEventArgs e)
{
this.Format();
}
private void MouseClicked(object sender, MouseButtonEventArgs e)
{
this.Format();
// Prevent changing the caret index
this.CaretIndex = this.Text.Length;
e.Handled = true;
}
private void MouseReleased(object sender, MouseButtonEventArgs e)
{
this.Format();
// Prevent changing the caret index
this.CaretIndex = this.Text.Length;
e.Handled = true;
}
private void KeyPressed(object sender, KeyEventArgs e)
{
if (IsValidKey(e.Key))
e.Handled = true;
if (Keyboard.Modifiers != ModifierKeys.None)
return;
this.Format();
}
private void PastingEventHandler(object sender, DataObjectEventArgs e)
{
// Prevent copy/paste
e.CancelCommand();
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
// Disable copy/paste
DataObject.AddCopyingHandler(this, PastingEventHandler);
DataObject.AddPastingHandler(this, PastingEventHandler);
this.CaretIndex = this.Text.Length;
this.PreviewKeyUp += KeyPressed;
this.PreviewMouseDown += MouseClicked;
this.PreviewMouseUp += MouseReleased;
this.TextChanged += ValueChanged;
this.Format();
}
}
这是 XAML:
<MyNamespace:CurrencyTextBox x:Name="TxbCurrency" Text="{Binding Path=DataContext.Element.Currency, ValidatesOnDataErrors=True}" />
到目前为止一切顺利!从小数 属性 到 TextBox 文本的绑定是 "right on"。但是如何在编辑后从文本中取回小数是现在的问题。
从十进制到 .Text 的绑定使用装箱来隐藏 ToString() 方法。
这里有问题:
在这种情况下,如何从 decimal 重载 Parse() 方法以使用我的 FormatBack() 方法从 TextBox 的文本中获取 decimal?
看看这篇文章,我认为它会对你有所帮助。
http://www.codeproject.com/Articles/15239/Validation-in-Windows-Presentation-Foundation
或者你可以把这个
private static bool IsTextAllowed(string text)
{
Regex regex = new Regex("[^0-9.-]+"); //regex that matches disallowed text
return !regex.IsMatch(text);
}
并在 PreviewTextInput
事件中将此
e.Handled = !IsTextAllowed(e.Text);
像这样创建新的Dependency Property
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(
"Value",
typeof(decimal?),
typeof(CurrencyTextBox),
new FrameworkPropertyMetadata(
new decimal?(),
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
new PropertyChangedCallback(ValuePropertyChanged)));
private static void ValuePropertyChanged(
DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
CurrencyTextBox x = (CurrencyTextBox)d;
x.Value = (decimal?)e.NewValue;
}
然后绑定到这个新的属性
好吧,为了以后的目的,如果有人遇到同样的麻烦,这里是货币文本框的完整代码。随意使用它,修改它,出售它(不要认为它有价值,你),或者想怎么玩就怎么玩!
/*
* the necessary usings:
* using System.Globalization;
* using System.Windows;
* using System.Windows.Controls;
* using System.Windows.Input;
* using System.Threading;
* And don't forget to change the currency settings on the XAML
* or in the defaults (on the contructor)
* It's set by default to Brazilian Real (R$)
*/
public class CurrencyTextBox : TextBox
{
public CurrencyTextBox()
{
CurrencySymbol = "R$ ";
CurrencyDecimalPlaces = 2;
DecimalSeparator = ",";
ThousandSeparator = ".";
Culture = "pt-BR";
}
public string CurrencySymbol { get; set; }
private int CurrencyDecimalPlaces { get; set; }
public string DecimalSeparator { get; set; }
public string ThousandSeparator { get; set; }
public string Culture { get; set; }
private bool IsValidKey(int k)
{
return (k >= 34 && k <= 43) //digits 0 to 9
|| (k >= 74 && k <= 83) //numeric keypad 0 to 9
|| (k == 2) //back space
|| (k == 32) //delete
;
}
private string Format(string text)
{
string unformatedString = text == string.Empty ? "0,00" : text; //Initial state is always string.empty
unformatedString = unformatedString.Replace(CurrencySymbol, ""); //Remove currency symbol from text
unformatedString = unformatedString.Replace(DecimalSeparator, ""); //Remove separators (decimal)
unformatedString = unformatedString.Replace(ThousandSeparator, ""); //Remove separators (thousands)
decimal number = decimal.Parse(unformatedString) / (decimal)Math.Pow(10, CurrencyDecimalPlaces); //The value will have 'x' decimal places, so divide it by 10^x
unformatedString = number.ToString("C", CultureInfo.CreateSpecificCulture(Culture));
return unformatedString;
}
private decimal FormatBack(string text)
{
string unformatedString = text == string.Empty ? "0.00" : text;
unformatedString = unformatedString.Replace(CurrencySymbol, ""); //Remove currency symbol from text
unformatedString = unformatedString.Replace(ThousandSeparator, ""); //Remove separators (thousands);
CultureInfo current = Thread.CurrentThread.CurrentUICulture; //Let's change the culture to avoid "Input string was in an incorrect format"
Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(Culture);
decimal returnValue = decimal.Parse(unformatedString);
Thread.CurrentThread.CurrentUICulture = current; //And now change it back, cuz we don't own the world, right?
return returnValue;
}
private void ValueChanged(object sender, TextChangedEventArgs e)
{
// Keep the caret at the end
this.CaretIndex = this.Text.Length;
}
private void MouseClicked(object sender, MouseButtonEventArgs e)
{
// Prevent changing the caret index
e.Handled = true;
this.Focus();
}
private void MouseReleased(object sender, MouseButtonEventArgs e)
{
// Prevent changing the caret index
e.Handled = true;
this.Focus();
}
private void KeyReleased(object sender, KeyEventArgs e)
{
this.Text = Format(this.Text);
this.Value = FormatBack(this.Text);
}
private void KeyPressed(object sender, KeyEventArgs e)
{
if (IsValidKey((int)e.Key))
return;
e.Handled = true;
this.CaretIndex = this.Text.Length;
}
private void PastingEventHandler(object sender, DataObjectEventArgs e)
{
// Prevent/disable paste
e.CancelCommand();
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
DataObject.AddCopyingHandler(this, PastingEventHandler);
DataObject.AddPastingHandler(this, PastingEventHandler);
this.CaretIndex = this.Text.Length;
this.KeyDown += KeyPressed;
this.KeyUp += KeyReleased;
this.PreviewMouseDown += MouseClicked;
this.PreviewMouseUp += MouseReleased;
this.TextChanged += ValueChanged;
this.Text = Format(string.Empty);
}
public decimal? Value
{
get { return (decimal?)this.GetValue(ValueProperty); }
set { this.SetValue(ValueProperty, value); }
}
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
"Value",
typeof(decimal?),
typeof(CurrencyTextBox),
new FrameworkPropertyMetadata(new decimal?(), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, new PropertyChangedCallback(ValuePropertyChanged)));
private static void ValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((CurrencyTextBox)d).Value = ((CurrencyTextBox)d).FormatBack(e.NewValue.ToString());
}
}
和 xaml:
<myNamespace:CurrencyTextBox
Value="{Binding Path=DataContext.MyDecimalProperty, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}"
CurrencySymbol="R$ "
Culture="pt-BR"
CurrencyDecimalPlaces="2"
DecimalSeparator=","
ThousandSeparator="." />
我认为这实际上是不可能的,除了只允许数字的框的简单情况。理想情况下,您想要一个只能包含有效条目的框,但小数包含一些本身无效的字符(如“-”和“.”)。如果不将框置于无效状态,则用户无法通过键入“-”开始。
同样,他们可以输入“1.”,然后删除 1 并使框处于不确定状态。当然,它会导致验证错误和红色边框,但您的视图模型仍然认为该值为 1,并且没有意识到这个问题。
对于正整数,您只能允许数字并在空白时自动插入一个零(虽然有点不友好)
对于小数和负整数,我认为你能做的最好的事情就是限制用户可以输入的键,但你仍然需要将你的数字 属性 包装在一个字符串中并验证它 - 无论何时OK 按钮被按下,或者理想情况下实现 INotifyDataError 以显示错误并禁用 OK 按钮。
我正在开发用于货币编辑的自定义文本框。
我见过一些现成可用的,但它们很复杂 and/or 并不能真正使用,迫使您采取不良做法(例如硬编码应该在控件上使用的名称)。
所以我决定自己做,但是我在使用绑定选项时遇到了麻烦,因为分配给绑定属性的 属性 必须是小数,但是 Text 属性 TextBox 控件接受字符串。
我想的答案是,也许,将访问方法(getter 和 setter)覆盖到 base class(TextBox)中的 Text 属性,但这是不允许的。
我的绑定应设置为值,即设置 TextBox 的文本 属性 随时随地将其格式化为文本(带有货币符号和所有内容),但在 Get 方法中将其转换回数字数据类型。
这是我到目前为止所取得的成就:
public class CurrencyTextBox : TextBox
{
private bool IsValidKey(Key key)
{
int k = (int)key;
return ((k >= 34 && k <= 43) //digits 0 to 9
|| (k >= 74 && k <= 83) //numeric keypad 0 to 9
|| (k == 2) //back space
|| (k == 32) //delete
);
}
private void Format()
{
//formatting decimal to currency text here
//Done! no problems here
}
private void FormatBack()
{
//formatting currency text to decimal here
//Done! no problems here
}
private void ValueChanged(object sender, TextChangedEventArgs e)
{
this.Format();
}
private void MouseClicked(object sender, MouseButtonEventArgs e)
{
this.Format();
// Prevent changing the caret index
this.CaretIndex = this.Text.Length;
e.Handled = true;
}
private void MouseReleased(object sender, MouseButtonEventArgs e)
{
this.Format();
// Prevent changing the caret index
this.CaretIndex = this.Text.Length;
e.Handled = true;
}
private void KeyPressed(object sender, KeyEventArgs e)
{
if (IsValidKey(e.Key))
e.Handled = true;
if (Keyboard.Modifiers != ModifierKeys.None)
return;
this.Format();
}
private void PastingEventHandler(object sender, DataObjectEventArgs e)
{
// Prevent copy/paste
e.CancelCommand();
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
// Disable copy/paste
DataObject.AddCopyingHandler(this, PastingEventHandler);
DataObject.AddPastingHandler(this, PastingEventHandler);
this.CaretIndex = this.Text.Length;
this.PreviewKeyUp += KeyPressed;
this.PreviewMouseDown += MouseClicked;
this.PreviewMouseUp += MouseReleased;
this.TextChanged += ValueChanged;
this.Format();
}
}
这是 XAML:
<MyNamespace:CurrencyTextBox x:Name="TxbCurrency" Text="{Binding Path=DataContext.Element.Currency, ValidatesOnDataErrors=True}" />
到目前为止一切顺利!从小数 属性 到 TextBox 文本的绑定是 "right on"。但是如何在编辑后从文本中取回小数是现在的问题。
从十进制到 .Text 的绑定使用装箱来隐藏 ToString() 方法。
这里有问题:
在这种情况下,如何从 decimal 重载 Parse() 方法以使用我的 FormatBack() 方法从 TextBox 的文本中获取 decimal?
看看这篇文章,我认为它会对你有所帮助。 http://www.codeproject.com/Articles/15239/Validation-in-Windows-Presentation-Foundation
或者你可以把这个
private static bool IsTextAllowed(string text)
{
Regex regex = new Regex("[^0-9.-]+"); //regex that matches disallowed text
return !regex.IsMatch(text);
}
并在 PreviewTextInput
事件中将此
e.Handled = !IsTextAllowed(e.Text);
像这样创建新的Dependency Property
public static readonly DependencyProperty ValueProperty =
DependencyProperty.Register(
"Value",
typeof(decimal?),
typeof(CurrencyTextBox),
new FrameworkPropertyMetadata(
new decimal?(),
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
new PropertyChangedCallback(ValuePropertyChanged)));
private static void ValuePropertyChanged(
DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
CurrencyTextBox x = (CurrencyTextBox)d;
x.Value = (decimal?)e.NewValue;
}
然后绑定到这个新的属性
好吧,为了以后的目的,如果有人遇到同样的麻烦,这里是货币文本框的完整代码。随意使用它,修改它,出售它(不要认为它有价值,你),或者想怎么玩就怎么玩!
/*
* the necessary usings:
* using System.Globalization;
* using System.Windows;
* using System.Windows.Controls;
* using System.Windows.Input;
* using System.Threading;
* And don't forget to change the currency settings on the XAML
* or in the defaults (on the contructor)
* It's set by default to Brazilian Real (R$)
*/
public class CurrencyTextBox : TextBox
{
public CurrencyTextBox()
{
CurrencySymbol = "R$ ";
CurrencyDecimalPlaces = 2;
DecimalSeparator = ",";
ThousandSeparator = ".";
Culture = "pt-BR";
}
public string CurrencySymbol { get; set; }
private int CurrencyDecimalPlaces { get; set; }
public string DecimalSeparator { get; set; }
public string ThousandSeparator { get; set; }
public string Culture { get; set; }
private bool IsValidKey(int k)
{
return (k >= 34 && k <= 43) //digits 0 to 9
|| (k >= 74 && k <= 83) //numeric keypad 0 to 9
|| (k == 2) //back space
|| (k == 32) //delete
;
}
private string Format(string text)
{
string unformatedString = text == string.Empty ? "0,00" : text; //Initial state is always string.empty
unformatedString = unformatedString.Replace(CurrencySymbol, ""); //Remove currency symbol from text
unformatedString = unformatedString.Replace(DecimalSeparator, ""); //Remove separators (decimal)
unformatedString = unformatedString.Replace(ThousandSeparator, ""); //Remove separators (thousands)
decimal number = decimal.Parse(unformatedString) / (decimal)Math.Pow(10, CurrencyDecimalPlaces); //The value will have 'x' decimal places, so divide it by 10^x
unformatedString = number.ToString("C", CultureInfo.CreateSpecificCulture(Culture));
return unformatedString;
}
private decimal FormatBack(string text)
{
string unformatedString = text == string.Empty ? "0.00" : text;
unformatedString = unformatedString.Replace(CurrencySymbol, ""); //Remove currency symbol from text
unformatedString = unformatedString.Replace(ThousandSeparator, ""); //Remove separators (thousands);
CultureInfo current = Thread.CurrentThread.CurrentUICulture; //Let's change the culture to avoid "Input string was in an incorrect format"
Thread.CurrentThread.CurrentUICulture = CultureInfo.CreateSpecificCulture(Culture);
decimal returnValue = decimal.Parse(unformatedString);
Thread.CurrentThread.CurrentUICulture = current; //And now change it back, cuz we don't own the world, right?
return returnValue;
}
private void ValueChanged(object sender, TextChangedEventArgs e)
{
// Keep the caret at the end
this.CaretIndex = this.Text.Length;
}
private void MouseClicked(object sender, MouseButtonEventArgs e)
{
// Prevent changing the caret index
e.Handled = true;
this.Focus();
}
private void MouseReleased(object sender, MouseButtonEventArgs e)
{
// Prevent changing the caret index
e.Handled = true;
this.Focus();
}
private void KeyReleased(object sender, KeyEventArgs e)
{
this.Text = Format(this.Text);
this.Value = FormatBack(this.Text);
}
private void KeyPressed(object sender, KeyEventArgs e)
{
if (IsValidKey((int)e.Key))
return;
e.Handled = true;
this.CaretIndex = this.Text.Length;
}
private void PastingEventHandler(object sender, DataObjectEventArgs e)
{
// Prevent/disable paste
e.CancelCommand();
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
DataObject.AddCopyingHandler(this, PastingEventHandler);
DataObject.AddPastingHandler(this, PastingEventHandler);
this.CaretIndex = this.Text.Length;
this.KeyDown += KeyPressed;
this.KeyUp += KeyReleased;
this.PreviewMouseDown += MouseClicked;
this.PreviewMouseUp += MouseReleased;
this.TextChanged += ValueChanged;
this.Text = Format(string.Empty);
}
public decimal? Value
{
get { return (decimal?)this.GetValue(ValueProperty); }
set { this.SetValue(ValueProperty, value); }
}
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(
"Value",
typeof(decimal?),
typeof(CurrencyTextBox),
new FrameworkPropertyMetadata(new decimal?(), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, new PropertyChangedCallback(ValuePropertyChanged)));
private static void ValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
((CurrencyTextBox)d).Value = ((CurrencyTextBox)d).FormatBack(e.NewValue.ToString());
}
}
和 xaml:
<myNamespace:CurrencyTextBox
Value="{Binding Path=DataContext.MyDecimalProperty, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}"
CurrencySymbol="R$ "
Culture="pt-BR"
CurrencyDecimalPlaces="2"
DecimalSeparator=","
ThousandSeparator="." />
我认为这实际上是不可能的,除了只允许数字的框的简单情况。理想情况下,您想要一个只能包含有效条目的框,但小数包含一些本身无效的字符(如“-”和“.”)。如果不将框置于无效状态,则用户无法通过键入“-”开始。
同样,他们可以输入“1.”,然后删除 1 并使框处于不确定状态。当然,它会导致验证错误和红色边框,但您的视图模型仍然认为该值为 1,并且没有意识到这个问题。
对于正整数,您只能允许数字并在空白时自动插入一个零(虽然有点不友好)
对于小数和负整数,我认为你能做的最好的事情就是限制用户可以输入的键,但你仍然需要将你的数字 属性 包装在一个字符串中并验证它 - 无论何时OK 按钮被按下,或者理想情况下实现 INotifyDataError 以显示错误并禁用 OK 按钮。