在表單驗證、資料錄入介面中,常需限制 TextBox 僅接受數字(0-9)。本文分析常見作法優缺點,並提供健全的 TextChanged 過濾實作,解決中文輸入法、貼上、拖曳等繞過鍵盤事件的問題。
| 方法 | 優點 | 缺點 |
|---|---|---|
KeyPress 攔截 | 簡單、即時 | 無法阻擋貼上、拖曳、中文輸入法確認、自動完成 |
KeyDown + e.SuppressKeyPress | 可攔截功能鍵 | 同樣無法處理非鍵盤輸入 |
MaskedTextBox | 內建遮罩 | 彈性低、體驗差、不支援動態長度 |
TextChanged 過濾 | 攔截所有文字變更來源 | 需處理游標位置、避免無限迴圈 |
WPF PreviewTextInput + 資料繫結驗證 | 架構乾淨 | 僅限 WPF |
private void TxtNumeric_TextChanged(object sender, EventArgs e)
{
var tb = (TextBox)sender;
string text = tb.Text;
// 快速路徑:已為純數字或空字串
if (string.IsNullOrEmpty(text) || IsAllDigits(text)) return;
// 記錄游標位置(相對於過濾後文字)
int selectionStart = tb.SelectionStart;
int digitsBeforeCursor = CountDigits(text, 0, selectionStart);
// 過濾非數字
string filtered = new string(text.Where(char.IsDigit).ToArray());
// 避免觸發遞迴:暫時移除事件
tb.TextChanged -= TxtNumeric_TextChanged;
tb.Text = filtered;
tb.TextChanged += TxtNumeric_TextChanged;
// 還原游標:盡量靠近原位置
tb.SelectionStart = Math.Min(digitsBeforeCursor, filtered.Length);
}
private bool IsAllDigits(string s) => s.All(char.IsDigit);
private int CountDigits(string s, int start, int count)
{
int end = Math.Min(start + count, s.Length);
int c = 0;
for (int i = start; i < end; i++) if (char.IsDigit(s[i])) c++;
return c;
}
tb.Text = filtered 再次觸發 TextChanged 造成無限迴圈或堆疊溢位char.IsDigit:支援全形數字(0-9),若僅要 ASCII 可改 c >= '0' && c <= '9'StringBuilder 或 char 陣列 就地修改private void TxtDecimal_TextChanged(object sender, EventArgs e)
{
var tb = (TextBox)sender;
string text = tb.Text;
if (string.IsNullOrEmpty(text)) return;
int selStart = tb.SelectionStart;
int validCharsBefore = CountValidChars(text, 0, selStart);
// 允許:數字、單一小數點、開頭負號
var sb = new StringBuilder();
bool decimalSeen = false;
for (int i = 0; i < text.Length; i++)
{
char c = text[i];
if (char.IsDigit(c)) sb.Append(c);
else if (c == '.' && !decimalSeen && sb.Length > 0) { sb.Append(c); decimalSeen = true; }
else if (c == '-' && i == 0 && sb.Length == 0) sb.Append(c);
}
string filtered = sb.ToString();
if (filtered == text) return;
tb.TextChanged -= TxtDecimal_TextChanged;
tb.Text = filtered;
tb.TextChanged += TxtDecimal_TextChanged;
tb.SelectionStart = Math.Min(validCharsBefore, filtered.Length);
}
private int CountValidChars(string s, int start, int count)
{
int end = Math.Min(start + count, s.Length);
int c = 0, dec = 0;
for (int i = start; i < end; i++)
{
char ch = s[i];
if (char.IsDigit(ch) || (ch == '.' && dec++ == 0) || (ch == '-' && i == 0)) c++;
}
return c;
}
封裝為可重用元件:
public class NumericTextBox : TextBox
{
public bool AllowDecimal { get; set; } = false;
public bool AllowNegative { get; set; } = false;
protected override void OnTextChanged(EventArgs e)
{
// 同前邏輯...
base.OnTextChanged(e);
}
}
<TextBox PreviewTextInput="OnPreviewTextInput"
Text="{Binding Amount, UpdateSourceTrigger=PropertyChanged,
ValidatesOnDataErrors=True}" />
// Code-behind
void OnPreviewTextInput(object sender, TextCompositionEventArgs e)
{
e.Handled = !Regex.IsMatch(e.Text, @"^\d+$"); // 僅允許數字
}
// ViewModel 驗證
public string Amount
{
get => _amount;
set
{
if (Regex.IsMatch(value, @"^\d*\.?\d*$")) _amount = value;
// else 保持舊值或顯示錯誤
}
}
TextChanged 過濾 + 事件暫停 + 游標還原,最健全MaskedTextBox 或 KeyPress 足夠PreviewTextInput