WPF TextBox输入限制实战:从数字校验到正则表达式的高级玩法
WPF TextBox输入限制实战:从数字校验到正则表达式的高级玩法
在WPF应用开发中,TextBox作为最基础的用户输入控件,其输入验证的健壮性直接决定了用户体验的质量。很多开发者都曾遇到过这样的场景:一个看似简单的“只能输入数字”的需求,在实际实现时却要面对输入法切换、粘贴操作、右键菜单、程序赋值等一系列边界情况。更复杂的是,当业务需求扩展到金额格式、手机号码、身份证号等特定格式时,简单的字符过滤已经无法满足要求。
这篇文章将带你深入WPF输入验证的核心机制,超越基础教程的范畴,重点探讨如何利用正则表达式构建灵活、可复用的输入限制方案。我们将从事件机制的差异分析入手,逐步构建一个能够应对真实业务场景的验证框架,最终封装成易于维护的附加属性。无论你是正在处理金融系统的金额输入,还是需要为CRM系统设计电话号码验证,这里提供的思路和代码都能直接应用到你的项目中。
1. 理解WPF输入事件的三层架构
要真正掌握TextBox的输入限制,首先需要理解WPF处理用户输入的三个关键事件:PreviewTextInput、PreviewKeyDown和TextChanged。这三个事件并非简单的替代关系,而是构成了一个完整的事件处理链条,各自负责不同的验证阶段。
1.1 PreviewTextInput:字符级别的实时拦截
PreviewTextInput事件在用户通过任何输入方式(键盘、手写、语音等)向控件输入文本时触发。这个事件的特点是它处理的是即将输入的文本内容,而不是已经显示在控件中的文本。
private void TextBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
// e.Text包含用户本次输入的字符
// 例如:用户输入"5",e.Text就是"5"
// 使用正则表达式验证单个字符
Regex digitRegex = new Regex(@"^[0-9]$");
e.Handled = !digitRegex.IsMatch(e.Text);
}
这里有一个关键细节:e.Handled = true表示事件已被处理,系统将不再继续传递该输入。这种机制让我们能够在字符进入TextBox之前就将其拦截。
注意:PreviewTextInput事件对于输入法(IME)的支持需要特别处理。当用户使用中文输入法时,输入的拼音并不会触发此事件,只有最终选定的汉字才会触发。这意味着如果你需要完全禁用非数字输入,还需要配合其他机制。
1.2 PreviewKeyDown:键盘操作的底层控制
与PreviewTextInput关注文本内容不同,PreviewKeyDown关注的是物理按键。这在某些场景下非常有用,比如需要区分数字键盘和主键盘区的数字键,或者需要处理功能键(如Delete、Backspace)。
private void TextBox_PreviewKeyDown(object sender, KeyEventArgs e)
{
// 允许的功能键
var allowedKeys = new[] {
Key.Back, Key.Delete, Key.Left, Key.Right,
Key.Home, Key.End, Key.Tab
};
// 允许的数字键(主键盘区和数字键盘区)
bool isDigit = (e.Key >= Key.D0 && e.Key <= Key.D9) ||
(e.Key >= Key.NumPad0 && e.Key <= Key.NumPad9);
// 允许小数点(注意不同键盘布局的差异)
bool isDecimalPoint = e.Key == Key.OemPeriod || e.Key == Key.Decimal;
// 允许Ctrl组合键(复制、粘贴等)
bool isCtrlCombination = (e.KeyboardDevice.Modifiers & ModifierKeys.Control) != 0;
e.Handled = !(isDigit || isDecimalPoint ||
allowedKeys.Contains(e.Key) || isCtrlCombination);
}
在实际项目中,我经常发现开发者忽略了对功能键的处理。想象一下用户无法使用退格键删除错误输入时的挫败感——这种细节决定了一个功能是否真正可用。
1.3 TextChanged:最终文本的完整性验证
前两个事件都是在输入过程中进行拦截,而TextChanged事件则在文本已经改变后触发。这看起来有些矛盾:既然已经阻止了非法输入,为什么还需要在改变后验证?
考虑以下场景:
- 通过代码直接设置
TextBox.Text属性 - 通过数据绑定更新文本框内容
- 用户使用鼠标右键菜单的粘贴功能(某些情况下可能绕过键盘事件)
- 从其他应用程序拖放文本到TextBox
private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
TextBox textBox = sender as TextBox;
// 验证完整的文本是否符合格式要求
Regex amountRegex = new Regex(@"^\d+(\.\d{0,2})?$");
if (!amountRegex.IsMatch(textBox.Text))
{
// 标记验证错误
Validation.SetError(textBox, "请输入有效的金额格式(最多两位小数)");
// 或者自动修正:移除非法字符
string cleanedText = new string(
textBox.Text.Where(c => char.IsDigit(c) || c == '.').ToArray()
);
// 避免递归触发TextChanged事件
textBox.TextChanged -= TextBox_TextChanged;
textBox.Text = cleanedText;
textBox.TextChanged += TextBox_TextChanged;
}
}
这三个事件构成了一个完整的防御体系:PreviewKeyDown处理物理按键,PreviewTextInput处理字符输入,TextChanged作为最终的安全网。在实际应用中,我建议根据具体需求选择合适的组合:
| 验证需求 | 推荐使用的事件 | 理由 |
|---|---|---|
| 简单字符过滤 | PreviewTextInput | 实现简单,能处理大多数输入场景 |
| 需要特殊按键控制 | PreviewKeyDown + PreviewTextInput | 如需要禁用空格键但允许Tab键 |
| 格式验证(如金额) | PreviewTextInput + TextChanged | 字符过滤+完整格式验证 |
| 最高安全性要求 | 所有三个事件 | 应对各种边界情况 |
2. 正则表达式的实战应用模式
正则表达式是文本验证的瑞士军刀,但在WPF输入限制中,它的使用方式需要根据具体场景进行调整。很多人直接套用网上的正则表达式,却忽略了WPF事件机制的特殊性。
2.1 单字符验证 vs 完整文本验证
这是最容易混淆的概念。在PreviewTextInput事件中,我们验证的是单个输入字符,而在TextChanged事件中,我们验证的是完整的文本内容。
// 单字符验证:只允许数字和小数点
private void ValidateSingleCharacter(object sender, TextCompositionEventArgs e)
{
// 这个正则匹配单个字符:数字或小数点
Regex singleCharRegex = new Regex(@"^[0-9.]$");
e.Handled = !singleCharRegex.IsMatch(e.Text);
}
// 完整文本验证:金额格式(最多两位小数)
private void ValidateCompleteText(object sender, TextChangedEventArgs e)
{
TextBox textBox = sender as TextBox;
// 这个正则匹配完整文本:整数部分 + 可选的小数部分(最多两位)
Regex completeTextRegex = new Regex(@"^\d+(\.\d{0,2})?$");
if (!completeTextRegex.IsMatch(textBox.Text))
{
// 处理验证失败
ShowValidationError(textBox, "格式错误:最多两位小数");
}
}
我曾经在一个金融项目中遇到过这样的问题:开发者在PreviewTextInput中使用了完整的金额正则表达式,结果用户无法输入小数点,因为单独的小数点字符"."不符合^\d+(\.\d{0,2})?$这个模式。正确的做法是在PreviewTextInput中只进行字符级过滤,在TextChanged中进行格式验证。
2.2 常见业务场景的正则表达式
不同的业务场景需要不同的验证策略。下面是一些我在实际项目中积累的验证模式:
手机号码验证(中国)
// 简单的手机号格式验证
Regex phoneRegex = new Regex(@"^1[3-9]\d{9}$");
// 更宽松的验证:允许带空格或连字符
Regex phoneWithSeparators = new Regex(@"^1[3-9]\d[\s\-]?\d{4}[\s\-]?\d{4}$");
// 在PreviewTextInput中的使用:允许数字、空格、连字符
private void PhoneNumber_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
Regex allowedChars = new Regex(@"^[0-9\s\-]$");
e.Handled = !allowedChars.IsMatch(e.Text);
}
身份证号码验证
// 简单的15位或18位身份证验证
Regex idCardRegex = new Regex(@"^(\d{15}|\d{17}[\dXx])$");
// 在TextChanged中的完整验证
private void ValidateIdCard(object sender, TextChangedEventArgs e)
{
TextBox textBox = sender as TextBox;
string input = textBox.Text.Replace(" ", ""); // 移除空格
if (idCardRegex.IsMatch(input))
{
// 还可以进行更详细的验证,如校验位计算
if (input.Length == 18 && !ValidateIdCardCheckDigit(input))
{
ShowValidationError(textBox, "身份证校验位错误");
}
}
else if (!string.IsNullOrEmpty(input))
{
ShowValidationError(textBox, "请输入15位或18位身份证号码");
}
}
金额输入的特殊处理 金额输入有几个特殊需求:
- 只能有一个小数点
- 小数点后最多两位
- 不能以0开头(除非是0本身或0.xx)
public class AmountInputValidator
{
private string previousText = "";
public void ValidateAmountInput(TextBox textBox, TextChangedEventArgs e)
{
string currentText = textBox.Text;
// 空文本是有效的
if (string.IsNullOrEmpty(currentText))
{
previousText = currentText;
return;
}
// 检查是否只包含数字和小数点
if (!Regex.IsMatch(currentText, @"^[0-9.]*$"))
{
textBox.Text = previousText;
textBox.CaretIndex = Math.Min(previousText.Length, textBox.CaretIndex);
return;
}
// 检查小数点的数量
int dotCount = currentText.Count(c => c == '.');
if (dotCount > 1)
{
textBox.Text = previousText;
textBox.CaretIndex = Math.Min(previousText.Length, textBox.CaretIndex);
return;
}
// 如果有小数点,检查小数位数
if (dotCount == 1)
{
int dotIndex = currentText.IndexOf('.');
string decimalPart = currentText.Substring(dotIndex + 1);
if (decimalPart.Length > 2)
{
textBox.Text = previousText;
textBox.CaretIndex = Math.Min(previousText.Length, textBox.CaretIndex);
return;
}
}
// 检查是否以0开头(排除0.xx的情况)
if (currentText.Length > 1 && currentText[0] == '0' && currentText[1] != '.')
{
textBox.Text = previousText;
textBox.CaretIndex = Math.Min(previousText.Length, textBox.CaretIndex);
return;
}
previousText = currentText;
}
}
这个金额验证器展示了如何在TextChanged事件中实现复杂的业务逻辑。它维护了一个previousText状态,在用户输入非法内容时能够回退到上一个有效状态。
2.3 正则表达式的性能优化
在频繁触发的事件(如PreviewTextInput)中使用正则表达式时,性能是需要考虑的因素。每次事件触发都创建新的Regex对象会产生不必要的开销。
public static class RegexCache
{
private static readonly ConcurrentDictionary<string, Regex> _cache =
new ConcurrentDictionary<string, Regex>();
public static Regex GetOrCreate(string pattern, RegexOptions options = RegexOptions.None)
{
string key = $"{pattern}|{options}";
return _cache.GetOrAdd(key, k =>
{
// 对于频繁使用的模式,使用Compiled选项提高性能
if (pattern.Length < 50) // 简单的模式才编译
{
return new Regex(pattern, options | RegexOptions.Compiled);
}
return new Regex(pattern, options);
});
}
}
// 使用缓存的正则表达式
private void TextBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
// 从缓存获取或创建正则表达式
Regex digitRegex = RegexCache.GetOrCreate(@"^[0-9]$");
e.Handled = !digitRegex.IsMatch(e.Text);
}
对于简单的字符验证,其实有更高效的方式:
// 替代方案:使用char方法,性能更好
private void ValidateDigitChar(object sender, TextCompositionEventArgs e)
{
// 检查每个字符是否为数字
foreach (char c in e.Text)
{
if (!char.IsDigit(c))
{
e.Handled = true;
return;
}
}
e.Handled = false;
}
选择哪种方式取决于具体需求:如果需要复杂的模式匹配,使用正则表达式;如果只是简单的字符分类,使用char类的方法性能更好。
3. 处理边界情况和用户体验细节
输入验证不仅仅是技术实现,更是用户体验设计。很多看似微小的细节,实际上对用户满意度有巨大影响。
3.1 粘贴操作的处理
用户习惯使用Ctrl+V粘贴内容,但粘贴操作可能绕过PreviewTextInput事件。WPF提供了专门处理粘贴的机制:
public static void SetupPasteHandling(TextBox textBox, Func<string, bool> validationFunc)
{
// 添加粘贴处理器
DataObject.AddPastingHandler(textBox, (sender, e) =>
{
if (e.DataObject.GetDataPresent(DataFormats.Text))
{
string pasteText = e.DataObject.GetData(DataFormats.Text) as string;
if (!string.IsNullOrEmpty(pasteText))
{
// 验证粘贴内容
if (!validationFunc(pasteText))
{
e.CancelCommand(); // 阻止粘贴
// 可选:给用户反馈
SystemSounds.Beep.Play();
// 或者显示提示
ShowTooltip(textBox, "粘贴内容包含非法字符");
}
}
}
});
}
// 使用示例:只允许粘贴数字
SetupPasteHandling(myTextBox, text =>
{
return text.All(char.IsDigit);
});
更友好的做法是允许粘贴,但自动过滤掉非法字符:
DataObject.AddPastingHandler(textBox, (sender, e) =>
{
if (e.DataObject.GetDataPresent(DataFormats.Text))
{
string originalText = e.DataObject.GetData(DataFormats.Text) as string;
if (!string.IsNullOrEmpty(originalText))
{
// 过滤非法字符
string filteredText = new string(
originalText.Where(c => char.IsDigit(c) || c == '.').ToArray()
);
if (filteredText != originalText)
{
// 用过滤后的文本替换粘贴内容
e.DataObject.SetData(DataFormats.Text, filteredText);
// 可选:通知用户
if (!string.IsNullOrEmpty(filteredText))
{
ShowNotification($"已过滤非法字符,粘贴内容:{filteredText}");
}
}
}
}
});
3.2 输入法(IME)的兼容性
对于中文、日文等需要使用输入法的语言,直接禁用输入法可能不是最佳选择。更好的做法是允许输入法,但在最终提交时验证:
// 不禁用输入法,但在TextChanged中验证
private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
{
TextBox textBox = sender as TextBox;
// 检查是否正在使用输入法
if (InputMethod.Current.ImeState != ImeState.Off)
{
// 输入法激活时,放宽验证或提供实时提示
UpdateImeStatusIndicator(textBox, "输入法激活中...");
return;
}
// 输入法关闭时,进行严格验证
if (!ValidateFinalText(textBox.Text))
{
MarkAsInvalid(textBox);
}
}
对于必须禁用输入法的场景(如纯数字输入),除了设置InputMethod.IsInputMethodEnabled="False",还需要处理用户可能通过系统快捷键切换输入法的情况:
// 监听输入法状态变化
InputMethod.Current.ImeStateChanged += (s, e) =>
{
if (myTextBox.IsFocused && InputMethod.Current.ImeState != ImeState.Off)
{
// 强制关闭输入法
InputMethod.Current.ImeState = ImeState.Off;
// 给用户提示
ShowTemporaryMessage("此输入框不支持输入法,已切换为英文模式");
}
};
3.3 光标位置和选择文本的处理
当用户在TextBox中已有文本的中间插入字符,或者选择了部分文本后输入时,验证逻辑需要特别处理:
private void TextBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
TextBox textBox = sender as TextBox;
// 模拟输入后的完整文本
string currentText = textBox.Text;
int selectionStart = textBox.SelectionStart;
int selectionLength = textBox.SelectionLength;
// 移除选中的文本
string textBeforeSelection = currentText.Substring(0, selectionStart);
string textAfterSelection = currentText.Substring(selectionStart + selectionLength);
// 插入新输入的文本
string newText = textBeforeSelection + e.Text + textAfterSelection;
// 验证新文本
Regex validationRegex = new Regex(@"^\d+(\.\d{0,2})?$");
e.Handled = !validationRegex.IsMatch(newText);
}
这个实现考虑了文本选择的情况,确保验证的是最终文本,而不仅仅是新输入的字符。
3.4 提供实时反馈和错误提示
良好的验证体验应该给用户即时、清晰的反馈。WPF提供了多种方式:
使用Validation.ErrorTemplate
<Style TargetType="TextBox" x:Key="ValidatedTextBoxStyle">
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<DockPanel>
<Border BorderBrush="Red" BorderThickness="1">
<AdornedElementPlaceholder/>
</Border>
<TextBlock Foreground="Red" Margin="5,0,0,0"
Text="{Binding [0].ErrorContent, RelativeSource={RelativeSource Self}}"/>
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
实时提示(ToolTip)
private void UpdateValidationTooltip(TextBox textBox, bool isValid, string message)
{
if (!isValid)
{
ToolTipService.SetToolTip(textBox, message);
ToolTipService.SetInitialShowDelay(textBox, 0);
ToolTipService.SetShowDuration(textBox, 3000);
// 立即显示ToolTip
if (textBox.ToolTip != null)
{
ToolTip toolTip = textBox.ToolTip as ToolTip;
if (toolTip != null && !toolTip.IsOpen)
{
toolTip.PlacementTarget = textBox;
toolTip.Placement = PlacementMode.Bottom;
toolTip.IsOpen = true;
// 3秒后自动关闭
var timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(3) };
timer.Tick += (s, e) => { toolTip.IsOpen = false; timer.Stop(); };
timer.Start();
}
}
}
else
{
ToolTipService.SetToolTip(textBox, null);
}
}
视觉反馈(颜色变化)
private void UpdateVisualState(TextBox textBox, bool isValid)
{
if (!isValid)
{
textBox.Background = new SolidColorBrush(Color.FromArgb(30, 255, 0, 0)); // 浅红色背景
textBox.BorderBrush = Brushes.Red;
}
else
{
textBox.ClearValue(TextBox.BackgroundProperty);
textBox.ClearValue(TextBox.BorderBrushProperty);
}
}
4. 构建可复用的附加属性框架
当项目中需要多个不同验证规则的TextBox时,为每个控件单独编写事件处理代码会导致大量重复。通过附加属性(Attached Property)封装验证逻辑,可以极大提高代码的复用性和可维护性。
4.1 基础附加属性设计
让我们从最简单的“只允许数字”开始,逐步构建一个完整的验证框架:
public static class TextBoxValidation
{
#region AllowOnlyNumbers 附加属性
public static bool GetAllowOnlyNumbers(DependencyObject obj)
{
return (bool)obj.GetValue(AllowOnlyNumbersProperty);
}
public static void SetAllowOnlyNumbers(DependencyObject obj, bool value)
{
obj.SetValue(AllowOnlyNumbersProperty, value);
}
public static readonly DependencyProperty AllowOnlyNumbersProperty =
DependencyProperty.RegisterAttached(
"AllowOnlyNumbers",
typeof(bool),
typeof(TextBoxValidation),
new PropertyMetadata(false, OnAllowOnlyNumbersChanged));
private static void OnAllowOnlyNumbersChanged(
DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is TextBox textBox)
{
// 禁用输入法,确保只能输入英文数字
InputMethod.SetIsInputMethodEnabled(textBox, false);
// 移除右键菜单,防止粘贴
textBox.ContextMenu = null;
bool newValue = (bool)e.NewValue;
bool oldValue = (bool)e.OldValue;
if (newValue && !oldValue)
{
// 附加事件处理器
textBox.PreviewTextInput += OnNumberOnlyPreviewTextInput;
textBox.PreviewKeyDown += OnNumberOnlyPreviewKeyDown;
// 处理粘贴
DataObject.AddPastingHandler(textBox, OnNumberOnlyPasting);
// 处理TextChanged(用于程序赋值的情况)
textBox.TextChanged += OnNumberOnlyTextChanged;
}
else if (!newValue && oldValue)
{
// 移除事件处理器
textBox.PreviewTextInput -= OnNumberOnlyPreviewTextInput;
textBox.PreviewKeyDown -= OnNumberOnlyPreviewKeyDown;
DataObject.RemovePastingHandler(textBox, OnNumberOnlyPasting);
textBox.TextChanged -= OnNumberOnlyTextChanged;
}
}
}
private static void OnNumberOnlyPreviewTextInput(
object sender, TextCompositionEventArgs e)
{
// 验证每个字符是否为数字
e.Handled = e.Text.Any(c => !char.IsDigit(c));
}
private static void OnNumberOnlyPreviewKeyDown(
object sender, KeyEventArgs e)
{
// 允许数字键、功能键、导航键
bool isDigit = (e.Key >= Key.D0 && e.Key <= Key.D9) ||
(e.Key >= Key.NumPad0 && e.Key <= Key.NumPad9);
bool isFunctionKey = e.Key == Key.Back || e.Key == Key.Delete ||
e.Key == Key.Left || e.Key == Key.Right ||
e.Key == Key.Home || e.Key == Key.End ||
e.Key == Key.Tab || e.Key == Key.Enter;
// 允许Ctrl+A(全选)、Ctrl+C(复制)、Ctrl+X(剪切)
bool isCtrlCombination = (e.KeyboardDevice.Modifiers & ModifierKeys.Control) != 0;
e.Handled = !(isDigit || isFunctionKey || isCtrlCombination);
}
private static void OnNumberOnlyPasting(
object sender, DataObjectPastingEventArgs e)
{
if (e.DataObject.GetDataPresent(DataFormats.Text))
{
string text = e.DataObject.GetData(DataFormats.Text) as string;
if (!string.IsNullOrEmpty(text) && text.Any(c => !char.IsDigit(c)))
{
e.CancelCommand();
}
}
}
private static void OnNumberOnlyTextChanged(
object sender, TextChangedEventArgs e)
{
TextBox textBox = sender as TextBox;
// 移除所有非数字字符
string newText = new string(
textBox.Text.Where(char.IsDigit).ToArray()
);
if (newText != textBox.Text)
{
// 避免递归触发TextChanged
textBox.TextChanged -= OnNumberOnlyTextChanged;
textBox.Text = newText;
textBox.CaretIndex = newText.Length;
textBox.TextChanged += OnNumberOnlyTextChanged;
}
}
#endregion
}
使用这个附加属性非常简单:
<TextBox local:TextBoxValidation.AllowOnlyNumbers="True"
Width="200" Height="30"/>
4.2 支持正则表达式的通用验证属性
为了让框架更加灵活,我们可以添加对正则表达式的支持:
public static class TextBoxValidation
{
#region ValidationRegex 附加属性
public static string GetValidationRegex(DependencyObject obj)
{
return (string)obj.GetValue(ValidationRegexProperty);
}
public static void SetValidationRegex(DependencyObject obj, string value)
{
obj.SetValue(ValidationRegexProperty, value);
}
public static readonly DependencyProperty ValidationRegexProperty =
DependencyProperty.RegisterAttached(
"ValidationRegex",
typeof(string),
typeof(TextBoxValidation),
new PropertyMetadata(null, OnValidationRegexChanged));
private static void OnValidationRegexChanged(
DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is TextBox textBox)
{
string newPattern = e.NewValue as string;
string oldPattern = e.OldValue as string;
// 清理旧的验证逻辑
if (!string.IsNullOrEmpty(oldPattern))
{
DetachValidationHandlers(textBox);
}
// 设置新的验证逻辑
if (!string.IsNullOrEmpty(newPattern))
{
AttachValidationHandlers(textBox, newPattern);
}
}
}
private static void AttachValidationHandlers(TextBox textBox, string pattern)
{
// 存储验证模式,供事件处理器使用
textBox.SetValue(ValidationRegexPatternProperty, pattern);
// 附加事件处理器
textBox.PreviewTextInput += OnRegexPreviewTextInput;
textBox.PreviewKeyDown += OnRegexPreviewKeyDown;
DataObject.AddPastingHandler(textBox, OnRegexPasting);
textBox.TextChanged += OnRegexTextChanged;
// 初始验证
ValidateTextWithRegex(textBox, textBox.Text, pattern);
}
private static readonly DependencyProperty ValidationRegexPatternProperty =
DependencyProperty.RegisterAttached(
"ValidationRegexPattern",
typeof(string),
typeof(TextBoxValidation),
new PropertyMetadata(null));
private static void OnRegexPreviewTextInput(
object sender, TextCompositionEventArgs e)
{
TextBox textBox = sender as TextBox;
string pattern = textBox.GetValue(ValidationRegexPatternProperty) as string;
if (!string.IsNullOrEmpty(pattern))
{
// 模拟输入后的文本
string currentText = textBox.Text;
int caretIndex = textBox.CaretIndex;
// 处理文本选择
string textBeforeCaret = currentText.Substring(0, caretIndex);
string textAfterCaret = currentText.Substring(caretIndex + textBox.SelectionLength);
string newText = textBeforeCaret + e.Text + textAfterCaret;
// 使用正则验证
try
{
Regex regex = new Regex(pattern);
e.Handled = !regex.IsMatch(newText);
}
catch (ArgumentException)
{
// 正则表达式无效,不阻止输入
e.Handled = false;
}
}
}
private static void ValidateTextWithRegex(TextBox textBox, string text, string pattern)
{
if (string.IsNullOrEmpty(pattern)) return;
try
{
Regex regex = new Regex(pattern);
bool isValid = string.IsNullOrEmpty(text) || regex.IsMatch(text);
// 更新验证状态
Validation.SetError(textBox, isValid ? null : "输入格式不正确");
// 视觉反馈
UpdateValidationVisual(textBox, isValid);
}
catch (ArgumentException ex)
{
// 记录日志或显示错误
Debug.WriteLine($"正则表达式错误: {ex.Message}");
}
}
#endregion
#region 预定义验证类型
public enum ValidationType
{
None,
Integer, // 整数
Decimal, // 小数
PhoneNumber, // 手机号
Email, // 电子邮件
Custom // 自定义正则
}
public static ValidationType GetValidationType(DependencyObject obj)
{
return (ValidationType)obj.GetValue(ValidationTypeProperty);
}
public static void SetValidationType(DependencyObject obj, ValidationType value)
{
obj.SetValue(ValidationTypeProperty, value);
}
public static readonly DependencyProperty ValidationTypeProperty =
DependencyProperty.RegisterAttached(
"ValidationType",
typeof(ValidationType),
typeof(TextBoxValidation),
new PropertyMetadata(ValidationType.None, OnValidationTypeChanged));
private static void OnValidationTypeChanged(
DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is TextBox textBox)
{
ValidationType type = (ValidationType)e.NewValue;
// 根据类型设置对应的正则表达式
string pattern = GetPatternForValidationType(type);
SetValidationRegex(textBox, pattern);
// 根据类型设置其他属性
switch (type)
{
case ValidationType.Integer:
case ValidationType.Decimal:
InputMethod.SetIsInputMethodEnabled(textBox, false);
textBox.ContextMenu = null;
break;
case ValidationType.Email:
// 电子邮件允许输入法
InputMethod.SetIsInputMethodEnabled(textBox, true);
break;
}
}
}
private static string GetPatternForValidationType(ValidationType type)
{
return type switch
{
ValidationType.Integer => @"^-?\d+$",
ValidationType.Decimal => @"^-?\d+(\.\d{0,2})?$",
ValidationType.PhoneNumber => @"^1[3-9]\d{9}$",
ValidationType.Email => @"^[^@\s]+@[^@\s]+\.[^@\s]+$",
_ => null
};
}
#endregion
}
现在我们可以通过多种方式使用验证:
<!-- 方式1:直接使用预定义类型 -->
<TextBox local:TextBoxValidation.ValidationType="Integer"
Width="200" Height="30"/>
<!-- 方式2:使用自定义正则表达式 -->
<TextBox local:TextBoxValidation.ValidationRegex="^[A-Za-z0-9_]+$"
Width="200" Height="30"/>
<!-- 方式3:金额输入(两位小数) -->
<TextBox local:TextBoxValidation.ValidationRegex="^\d+(\.\d{0,2})?$"
Width="200" Height="30"/>
4.3 添加验证状态和错误信息
一个完整的验证框架还需要提供验证状态和错误信息:
public static class TextBoxValidation
{
#region 验证状态和错误信息
public static bool GetIsValid(DependencyObject obj)
{
return (bool)obj.GetValue(IsValidProperty);
}
private static void SetIsValid(DependencyObject obj, bool value)
{
obj.SetValue(IsValidPropertyKey, value);
}
private static readonly DependencyPropertyKey IsValidPropertyKey =
DependencyProperty.RegisterAttachedReadOnly(
"IsValid",
typeof(bool),
typeof(TextBoxValidation),
new PropertyMetadata(true));
public static readonly DependencyProperty IsValidProperty =
IsValidPropertyKey.DependencyProperty;
public static string GetErrorMessage(DependencyObject obj)
{
return (string)obj.GetValue(ErrorMessageProperty);
}
private static void SetErrorMessage(DependencyObject obj, string value)
{
obj.SetValue(ErrorMessagePropertyKey, value);
}
private static readonly DependencyPropertyKey ErrorMessagePropertyKey =
DependencyProperty.RegisterAttachedReadOnly(
"ErrorMessage",
typeof(string),
typeof(TextBoxValidation),
new PropertyMetadata(null));
public static readonly DependencyProperty ErrorMessageProperty =
ErrorMessagePropertyKey.DependencyProperty;
private static void UpdateValidationVisual(TextBox textBox, bool isValid, string message = null)
{
SetIsValid(textBox, isValid);
SetErrorMessage(textBox, isValid ? null : message);
// 更新WPF内置的验证系统
if (!isValid)
{
Validation.SetError(textBox, new ValidationError(
new RegexValidationRule(),
textBox.GetBindingExpression(TextBox.TextProperty),
message ?? "输入验证失败",
null
));
}
else
{
Validation.ClearInvalid(textBox.GetBindingExpression(TextBox.TextProperty));
}
// 触发属性变更通知,方便MVVM绑定
var binding = textBox.GetBindingExpression(IsValidProperty);
binding?.UpdateTarget();
}
#endregion
#region 验证规则(用于MVVM数据验证)
public class RegexValidationRule : ValidationRule
{
public string Pattern { get; set; }
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
string input = value as string;
if (string.IsNullOrEmpty(Pattern) || string.IsNullOrEmpty(input))
{
return ValidationResult.ValidResult;
}
try
{
if (Regex.IsMatch(input, Pattern))
{
return ValidationResult.ValidResult;
}
else
{
return new ValidationResult(false,
$"输入不符合格式要求。预期格式: {GetPatternDescription(Pattern)}");
}
}
catch (ArgumentException ex)
{
return new ValidationResult(false,
$"验证规则配置错误: {ex.Message}");
}
}
private string GetPatternDescription(string pattern)
{
// 将常见的正则模式转换为用户友好的描述
return pattern switch
{
@"^-?\d+$" => "整数",
@"^-?\d+(\.\d{0,2})?$" => "数字(最多两位小数)",
@"^1[3-9]\d{9}$" => "手机号码",
@"^[^@\s]+@[^@\s]+\.[^@\s]+$" => "电子邮件地址",
_ => "自定义格式"
};
}
}
#endregion
}
4.4 完整的使用示例
最后,让我们看一个完整的示例,展示如何在真实项目中使用这个验证框架:
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp.Validation"
Title="用户信息录入" Height="450" Width="400">
<Window.Resources>
<Style TargetType="TextBox" x:Key="ValidatedTextBox">
<Setter Property="Margin" Value="5"/>
<Setter Property="Padding" Value="3"/>
<Style.Triggers>
<Trigger Property="local:TextBoxValidation.IsValid" Value="False">
<Setter Property="BorderBrush" Value="Red"/>
<Setter Property="BorderThickness" Value="2"/>
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(local:TextBoxValidation.ErrorMessage)}"/>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 姓名(无限制) -->
<TextBlock Text="姓名:" Grid.Row="0" Grid.Column="0"
VerticalAlignment="Center" Margin="0,0,10,0"/>
<TextBox Grid.Row="0" Grid.Column="1"
Style="{StaticResource ValidatedTextBox}"/>
<!-- 年龄(只允许整数) -->
<TextBlock Text="年龄:" Grid.Row="1" Grid.Column="0"
VerticalAlignment="Center" Margin="0,0,10,0"/>
<TextBox Grid.Row="1" Grid.Column="1"
Style="{StaticResource ValidatedTextBox}"
local:TextBoxValidation.ValidationType="Integer"/>
<!-- 手机号 -->
<TextBlock Text="手机号:" Grid.Row="2" Grid.Column="0"
VerticalAlignment="Center" Margin="0,0,10,0"/>
<TextBox Grid.Row="2" Grid.Column="1"
Style="{StaticResource ValidatedTextBox}"
local:TextBoxValidation.ValidationType="PhoneNumber"/>
<!-- 邮箱 -->
<TextBlock Text="邮箱:" Grid.Row="3" Grid.Column="0"
VerticalAlignment="Center" Margin="0,0,10,0"/>
<TextBox Grid.Row="3" Grid.Column="1"
Style="{StaticResource ValidatedTextBox}"
local:TextBoxValidation.ValidationType="Email"/>
<!-- 金额(自定义正则) -->
<TextBlock Text="金额:" Grid.Row="4" Grid.Column="0"
VerticalAlignment="Center" Margin="0,0,10,0"/>
<TextBox Grid.Row="4" Grid.Column="1"
Style="{StaticResource ValidatedTextBox}"
local:TextBoxValidation.ValidationRegex="^\d+(\.\d{0,2})?$"/>
<!-- 提交按钮(根据验证状态启用) -->
<Button Grid.Row="5" Grid.Column="0" Grid.ColumnSpan="2"
Content="提交" HorizontalAlignment="Center"
Margin="0,20,0,0" Padding="20,5">
<Button.Style>
<Style TargetType="Button">
<Setter Property="IsEnabled" Value="False"/>
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<!-- 检查所有TextBox的验证状态 -->
<Condition Binding="{Binding ElementName=ageBox,
Path=(local:TextBoxValidation.IsValid)}"
Value="True"/>
<Condition Binding="{Binding ElementName=phoneBox,
Path=(local:TextBoxValidation.IsValid)}"
Value="True"/>
<Condition Binding="{Binding ElementName=emailBox,
Path=(local:TextBoxValidation.IsValid)}"
Value="True"/>
</MultiDataTrigger.Conditions>
<Setter Property="IsEnabled" Value="True"/>
</MultiDataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</Grid>
</Window>
这个完整的示例展示了如何将验证框架应用到实际界面中。通过附加属性,我们可以为每个TextBox指定验证规则,样式触发器会自动根据验证状态更新视觉反馈,提交按钮的状态也会根据所有输入框的验证结果自动更新。
在实际项目中,我通常会将这种验证框架与MVVM模式结合使用,通过绑定将验证状态同步到ViewModel中。这样既保持了UI的响应性,又确保了业务逻辑的清晰分离。
更多推荐
所有评论(0)