using System; using System.ComponentModel; using System.Drawing; using System.Globalization; using System.Media; using System.Text.RegularExpressions; using System.Windows.Forms; using System.Runtime.InteropServices; namespace Unclassified.UI { public class ValidatingTextBoxEventArgs : EventArgs { string text = ""; bool accept = true; /// /// Gets the text that should be validated or sets a new text to be inserted into the input field. /// public string Text { get { return text; } set { text = value; } } /// /// Gets or sets a value indicating whether the new text should be accepted. /// public bool Accept { get { return accept; } set { accept = value; } } public ValidatingTextBoxEventArgs(string text) { this.text = text; } } public enum ValidatingTextBoxType { /// /// No restriction on the text /// Default = 0, /// /// Only numeric text is allowed (digits from 0 to 9 only) /// Numeric, /// /// The list of valid characters is regarded, if not empty /// ValidCharacters, /// /// The regular expression is regarded, if not empty /// RegularExpression, /// /// The ValidateText event is fired to delegate the text validation /// Custom } public class ValidatingTextBox : TextBox { private string prevText = ""; private int prevSelStart = 0; private int prevSelLen = 0; private ValidatingTextBoxType type = ValidatingTextBoxType.Default; private string validCharacters = ""; private string regexString = ""; private Regex regex = null; private string finalRegexString = ""; private Regex finalRegex = null; private bool requireInput = false; private bool trimOnLeaving = false; private ToolTip errorNoticeTooltip = null; private string expectedFormatDescription = ""; private bool deferFinalCheck = false; private bool autoHeight = false; /// /// Validate the new text before it is applied. Only used if Type is Custom. /// [Category("Behavior")] [Description("Validate the new text before it is applied. Only used if Type is Custom.")] public event EventHandler ValidateText; /// /// Validate the new text before the focus leaves. /// [Category("Behavior")] [Description("Validate the new text before the focus leaves.")] public event EventHandler FinalValidateText; /// /// Standard type of text input to be accepted /// [Category("Behavior")] [Description("Standard type of text input to be accepted")] [DefaultValue(ValidatingTextBoxType.Default)] public ValidatingTextBoxType Type { get { return type; } set { type = value; } } /// /// Characters to be accepted /// [Category("Behavior")] [Description("Characters to be accepted")] [DefaultValue("")] public string ValidCharacters { get { return validCharacters; } set { validCharacters = value; } } /// /// Regex pattern to be accepted /// [Category("Behavior")] [Description("Regex to be accepted")] [DefaultValue("")] public string RegularExpression { get { return regexString; } set { if (!string.IsNullOrEmpty(value)) regex = new Regex(value); else regex = null; regexString = value; } } /// /// Regex pattern to be matched when leaving the focus /// [Category("Behavior")] [Description("Regex pattern to be matched when leaving the focus")] [DefaultValue("")] public string FinalRegularExpression { get { return finalRegexString; } set { if (!string.IsNullOrEmpty(value)) finalRegex = new Regex(value); else finalRegex = null; finalRegexString = value; } } /// /// Require input. Checked as part of the final check. /// [Category("Behavior")] [Description("Require input. Checked as part of the final check.")] [DefaultValue(false)] public bool RequireInput { get { return requireInput; } set { requireInput = value; } } /// /// Trim the text when leaving the focus /// [Category("Behavior")] [Description("Trim the text when leaving the focus")] [DefaultValue(false)] public bool TrimOnLeaving { get { return trimOnLeaving; } set { trimOnLeaving = value; } } /// /// Description of the expected format to be displayed to the user /// [Category("Behavior")] [Description("Description of the expected format to be displayed to the user")] [DefaultValue("")] public string ExpectedFormatDescription { get { return expectedFormatDescription; } set { expectedFormatDescription = value; } } /// /// Defer the final checks. They will only be applied when the FinalCheck method is invoked. /// [Category("Behavior")] [Description("Defer the final checks. They will only be applied when the FinalCheck method is invoked.")] [DefaultValue(false)] public bool DeferFinalCheck { get { return deferFinalCheck; } set { deferFinalCheck = value; } } /// /// Adjust the height of the control depending on its contents /// [Category("Layout")] [Description("Adjust the height of the control depending on its contents")] [DefaultValue(false)] public bool AutoHeight { get { return autoHeight; } set { autoHeight = value; UpdateAutoHeight(); } } protected override void OnTextChanged(EventArgs e) { bool accept = true; HideErrorNotice(); if (type == ValidatingTextBoxType.Numeric) { string valid = "0123456789"; foreach (char c in Text.ToCharArray()) { if (valid.IndexOf(c) == -1) { accept = false; break; } } } else if (type == ValidatingTextBoxType.ValidCharacters && !string.IsNullOrEmpty(validCharacters)) { foreach (char c in Text.ToCharArray()) { if (validCharacters.IndexOf(c) == -1) { accept = false; break; } } } else if (type == ValidatingTextBoxType.RegularExpression && regex != null) { accept = regex.IsMatch(Text); } else if (type == ValidatingTextBoxType.Custom && ValidateText != null) { ValidatingTextBoxEventArgs e2 = new ValidatingTextBoxEventArgs(Text); ValidateText(this, e2); accept = e2.Accept; } if (!accept) { SystemSounds.Beep.Play(); Console.Beep(50, 10); Text = prevText; SelectionStart = prevSelStart; SelectionLength = prevSelLen; ShowErrorNotice(); } else { prevText = Text; } base.OnTextChanged(e); UpdateAutoHeight(); } protected override void OnKeyDown(KeyEventArgs e) { // Add some comfort here if (e.KeyCode == Keys.Back && !e.Alt && e.Control && !e.Shift || e.KeyCode == Keys.W && !e.Alt && e.Control && !e.Shift) { // Delete word to the left int sel = SelectionStart; if (sel > 0) { int pos = Text.Substring(0, sel - 1).LastIndexOfAny(new char[] { ' ', '\t' }); //if (pos == -1) pos = 0; pos++; Text = Text.Substring(0, pos) + Text.Substring(sel); SelectionStart = pos; } e.SuppressKeyPress = e.Handled = true; } if (e.KeyCode == Keys.U && !e.Alt && e.Control && !e.Shift) { // Delete all Text = ""; e.SuppressKeyPress = e.Handled = true; } if (e.KeyCode == Keys.A && !e.Alt && e.Control && !e.Shift) { // Select all SelectionStart = 0; SelectionLength = Text.Length; e.SuppressKeyPress = e.Handled = true; } if (e.KeyCode == Keys.Up && !e.Alt && e.Control && !e.Shift && Multiline) { // Scroll line up SendMessage(Handle, EM_SCROLL, new IntPtr(SB_LINEUP), IntPtr.Zero); e.SuppressKeyPress = e.Handled = true; } if (e.KeyCode == Keys.Down && !e.Alt && e.Control && !e.Shift && Multiline) { // Scroll line down SendMessage(Handle, EM_SCROLL, new IntPtr(SB_LINEDOWN), IntPtr.Zero); e.SuppressKeyPress = e.Handled = true; } prevSelStart = SelectionStart; prevSelLen = SelectionLength; base.OnKeyDown(e); } protected override void OnKeyUp(KeyEventArgs e) { base.OnKeyUp(e); prevSelStart = SelectionStart; prevSelLen = SelectionLength; } protected override void OnMouseDown(MouseEventArgs e) { prevSelStart = SelectionStart; prevSelLen = SelectionLength; base.OnMouseDown(e); } protected override void OnLeave(EventArgs e) { if (trimOnLeaving) { Text = Text.Trim(); } HideErrorNotice(); if (!deferFinalCheck) { FinalCheck(); } base.OnLeave(e); } protected override void OnResize(EventArgs e) { base.OnResize(e); UpdateAutoHeight(); } /// /// Performs the deferred final checks. Shows the error message, plays the beep sound and focuses the control if errors were found. /// /// true if the check is successful, false if errors were found public bool FinalCheck() { bool accept = true; HideErrorNotice(); if (accept && requireInput && Text.Length == 0) { accept = false; } if (accept && finalRegex != null) { accept = finalRegex.IsMatch(Text); } if (accept && FinalValidateText != null) { ValidatingTextBoxEventArgs e2 = new ValidatingTextBoxEventArgs(Text); FinalValidateText(this, e2); accept = e2.Accept; Text = e2.Text; } if (!accept) { ShowErrorNotice(); SystemSounds.Beep.Play(); Console.Beep(50, 10); Focus(); } return accept; } protected void ShowErrorNotice() { string msg; switch (CultureInfo.CurrentUICulture.TwoLetterISOLanguageName) { case "de": msg = "Die Eingabe hat ein ungültiges Format."; if (!string.IsNullOrEmpty(expectedFormatDescription)) msg += "\r\nErwartet: " + expectedFormatDescription; else if (Type == ValidatingTextBoxType.Numeric) msg += "\r\nEs sind nur nummerische Eingaben (0-9) zulässig."; if (requireInput) msg += "\r\nEine Eingabe ist erforderlich."; break; default: msg = "The input format is invalid."; if (!string.IsNullOrEmpty(expectedFormatDescription)) msg += "\r\nExpected: " + expectedFormatDescription; else if (Type == ValidatingTextBoxType.Numeric) msg += "\r\nOnly numeric values (0-9) are accepted."; if (requireInput) msg += "\r\nInput is required."; break; } if (errorNoticeTooltip != null) { errorNoticeTooltip.Dispose(); } errorNoticeTooltip = new ToolTip(); errorNoticeTooltip.ForeColor = SystemColors.WindowText; errorNoticeTooltip.BackColor = MixedColor(SystemColors.Window, Color.Red, 0.25); errorNoticeTooltip.Show(msg, this, 0, Height, 10000); } protected void HideErrorNotice() { if (errorNoticeTooltip != null) { errorNoticeTooltip.Dispose(); } } private void UpdateAutoHeight() { if (autoHeight && Multiline) { Size prefSize = GetPreferredSize(new Size(Width, 0)); Height = prefSize.Height; } } #region Unclassified.WinApi [DllImport("user32")] private static extern IntPtr SendMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); private const uint EM_SCROLL = 0xB5; private const uint SB_LINEUP = 0; private const uint SB_LINEDOWN = 1; #endregion #region Unclassified.Drawing.ColorMath private static Color MixedColor(Color color1, Color color2, double ratio) { int a = (int) Math.Round(color1.A * (1 - ratio) + color2.A * ratio); int r = (int) Math.Round(color1.R * (1 - ratio) + color2.R * ratio); int g = (int) Math.Round(color1.G * (1 - ratio) + color2.G * ratio); int b = (int) Math.Round(color1.B * (1 - ratio) + color2.B * ratio); return Color.FromArgb(a, r, g, b); } #endregion } }