Answering my own question while providing my humble code:
Info (Last Update: 2022.04.02)
About
- This is the Default TextBox with Extra Features.
Remarks
- I've been working on this control recently and I'm adding more features on the go. The control should be working fine, even though it may require some further development.
- I'll try to keep it updated as soon as possible until perfection is achieved.
See "Known Bugs" bellow.
Features:
- Filter / Format / Validate the Text Input (Text, Numeric, Currency or IP Address).
- Set Currency Designator Symbol.
- Set Currency Designator as a Symbol or Abbreviated Designator Name (i.e: EUR).
- Set the Currency Designator Symbol Location.
i.e:
Left: Before the Value.
Right: After the Value.
- Set Values as Decimal.
- Set Decimal Zeros Automatically when Entering a Whole Number.
- Limit Maximum Character Input
Bug Fixes
- Prevented Clipboard Data to be Set to the Control.
- Default TextBox Initial (Default) Value was Impossible to be Set (for Numeric and Currency Text Inputs).
- Character Limiter Function was Missing.
Known Bugs
- There is an issue with Text (number of chars) Limiter while using Decimals. TextBox Prevents user Text Input Proper Behaviour.
- Sometimes not Accepting Paste.
SHIFT + INSERT, on the other hand is allowed.
Code
using System;
using System.ComponentModel;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
namespace RG_Custom_Controls.Controls
{
public class RGTextBoxI : TextBox
{
#region <Constructor>
public RGTextBoxI()
{
// -> Set Default Configuration.
ForeColor = Color.Gainsboro;
BackColor = Color.FromArgb(255, 36, 36, 52);
BorderStyle = BorderStyle.FixedSingle;
}
#endregion
#region <Fields>
private const int WM_PASTE = 0x0302; // Used to Validate Clipboar Data.
private string numbers = "0123456789.";
private string allowedChars => numbers;
private string decimalFormat = string.Empty;
#endregion
#region <Custom Properties> : (Char Limiter)
private bool charsLimited = false;
[Category("1. Custom Properties"), DisplayName("1. Chars Limited")]
[Description("Toggle Character Input Limit.")]
[Browsable(true)]
public bool CharsLimited
{
get { return charsLimited; }
set { charsLimited = value; }
}
private int maximumChars = 32;
[Category("1. Custom Properties"), DisplayName("2. Maximum Chars")]
[Description("Limit the Maximum Number of Chars Allowed.")]
[Browsable(true)]
public int MaximumChars
{
get { return maximumChars; }
set { maximumChars = value; }
}
#endregion
#region <Custom Properties> : (Input Mode)
/// <summary> TextBox Text Iput Mode (Normal, Numeric, Currency). </summary>
public enum TextBoxInputType { Default, Numeric, Currency, IPV4 }
private TextBoxInputType inputType = TextBoxInputType.Default;
[Category("1. Custom Properties"), DisplayName("1. Input Mode")]
[Description("Select Control Mode (Normal, Numeric or Currency).")]
[Bindable(true)] /* Required for Enum Types */
[Browsable(true)]
public TextBoxInputType TextBoxType
{
get { return inputType; }
set
{
inputType = value;
Text_SetDefaultValue();
Text_Align();
Invalidate();
}
}
#endregion
#region <Custom Properties> : (Decimals)
private bool useDecimals;
[Category("1. Custom Properties"), DisplayName("2. Use Decimals")]
[Description("Select wether to use Whole Number or a Decimal Number.")]
[Browsable(true)]
public bool UseDecimals
{
get { return useDecimals; }
set { useDecimals = value; }
}
private int decimalPlaces = 2;
[Category("1. Custom Properties"), DisplayName("3. Decimal Places")]
[Description("Select wether to use Whole Number or a Decimal Number.")]
[Browsable(true)]
public int DecimalPlaces
{
get { return decimalPlaces; }
set
{
if (value > 0 & value < 3)
{
decimalPlaces = value;
// Aet Decimal Format
switch (decimalPlaces)
{
case 1: decimalFormat = "0.0"; break;
case 2: decimalFormat = "0.00"; break;
}
}
}
}
#endregion
#region <Custom Properties> : (Curency Designator)
private string currencyDesignator = "€";
[Category("1. Custom Properties"), DisplayName("4. Currency Designator")]
[Description("Set Currency Symbol or Designator.\n\n i.e: €, Eur, Euros")]
[Browsable(true)]
public string CurrencyDesignator
{
get { return currencyDesignator; }
set { currencyDesignator = value; }
}
public enum DesignatorAlignment { Left, Right }
private DesignatorAlignment designatorAlignment = DesignatorAlignment.Right;
[Category("1. Custom Properties"), DisplayName("5. Designator Location")]
[Description("Select Currency Designator Location")]
[Bindable(true)] /* Required for Enum Types */
[Browsable(true)]
public DesignatorAlignment DesignatorLocation
{
get { return designatorAlignment; }
set { designatorAlignment = value; }
}
#endregion
private bool IsLimitingChars(int textLength)
{
bool val = false;
if (charsLimited)
{
switch (inputType)
{
case TextBoxInputType.Default: val = Text.Length.Equals(maximumChars); break;
case TextBoxInputType.Numeric:
case TextBoxInputType.Currency:
if (useDecimals)
{
// Note: '+1' Refers the '.' that Separates the Decimals
val = Text.Length.Equals(maximumChars + decimalPlaces + 1);
}
else { val = Text.Length.Equals(maximumChars); }
break;
}
// case TextBoxInputType.IPV4: break;
}
return val;
}
private void SetDecimalValue()
{
Text_RemoveWhiteSpaces();
Text_SetDecimalValue();
Text_AddCurrencyDesignator();
}
#region <Overriden Events>
/// <summary> Occurs Before the Control Stops Being the Active Control. </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
protected override void OnValidating(CancelEventArgs e)
{
base.OnValidating(e);
switch (inputType)
{
// ...
case TextBoxInputType.IPV4:
// Validate the IPv4 Address
if (!HasValidIPAddress(Text)) { Text_SetDefaultValue(); }
break;
}
}
/// <summary> Occurs when a Keyboard Key is Pressed. </summary>
/// <param name="e"></param>
protected override void OnKeyPress(KeyPressEventArgs e)
{
base.OnKeyPress(e);
if (!e.KeyChar.Equals((char)Keys.Back))
{
// Limit Number of Characters
switch (inputType)
{
// case TextBoxInputType.Default: e.Handled = IsLimitingChars(Text.Length); break;
case TextBoxInputType.Numeric:
case TextBoxInputType.Currency:
e.Handled = !HasValidNumericChar(e.KeyChar) ^ IsLimitingChars(Text.Length);
if (e.KeyChar.Equals('.') & NrCharOccurrences('.') >= 1) { e.Handled = true; }
break;
// ...
}
}
}
/// <summary> Occurs when the Control Becomes the Active Control. </summary>
/// <param name="e"></param>
protected override void OnEnter(EventArgs e)
{
base.OnEnter(e);
switch (inputType)
{
// ...
case TextBoxInputType.Currency:
Text_RemoveWhiteSpaces();
Text_RemoveCurrencyDesignator();
break;
// ...
}
// Select the Text
SelectAll();
}
/// <summary> Occurs when the Control Stops Being the Active Control. </summary>
/// <param name="e"></param>
protected override void OnLeave(EventArgs e)
{
base.OnLeave(e);
switch (inputType)
{
// ...
case TextBoxInputType.Currency:
SetDecimalValue();
break;
// ...
}
}
#endregion
#region <Methods> : (Validate Clipboard Data : On Paste)
protected override void WndProc(ref Message m)
{
/*
* Remarks: Handling Clipboard Data (Validate Data on Paste).
* Adapted Code from: 'Thorarin'.
* Source: https://stackoverflow.com/questions/15987712/handle-a-paste-event-in-c-sharp
*/
// 1. Handle All Other Messages Normally.
if (m.Msg != WM_PASTE) { base.WndProc(ref m); }
// 2. Handle Clipboard Data (On Paste).
else
{
if (Clipboard.ContainsText())
{
string val = Clipboard.GetText();
if (HasValidClipboardContent(val)) { Text = val; }
// Note(s):
// Text Validation for Each Input Type, Occurs under Control Leave Event.
// Clipboard.Clear(); --> You can use this if you Wish to Clear the Clipboard after Pasting the Value
}
}
}
#endregion
// 65666
#region <Methods>
/// <summary> Determines if the Clipboard Content Value is Valid. </summary>
/// <param name="val"></param>
/// <returns> True if Clipboard Content Matches the TextBox Input Requirements. </returns>
private bool HasValidClipboardContent(string val)
{
bool isValid = false;
switch (inputType)
{
case TextBoxInputType.Default: isValid = !IsLimitingChars(val.Length); break;
case TextBoxInputType.Numeric:
case TextBoxInputType.Currency:
isValid = !IsLimitingChars(val.Length) && IsNumericString(val);
break;
case TextBoxInputType.IPV4:
isValid = HasValidIPAddress(val);
break;
}
return isValid;
}
/// <summary> Determines if Specified Char Paramter is a Valid Numeric Character. </summary>
/// <param name="char"></param>
/// <returns> true if Received Char is a Number. </returns>
private bool HasValidNumericChar(char @char)
{
return allowedChars.Contains(@char) | @char.Equals((char)Keys.Back);
}
/// <summary> Determines if Received String Parameter is a Number. </summary>
/// <param name="value"></param>
/// <returns> True if Received String Parameter is a Number. </returns>
private bool IsNumericString(string value)
{
bool isNumeric = true;
for (int i = 0; i < value.Length; i++)
{
char c = value[i];
if (!HasValidNumericChar(c))
{
isNumeric = false;
break;
}
}
return isNumeric;
}
/// <summary> Determines if Specified Parameter String Contains a Valid IPv4 Address. </summary>
/// <returns> True if the IPv4 Address is Valid. </returns>
private bool HasValidIPAddress(string value)
{
// Remarks:
// Code based on Yiannis Leoussis Approach.
// Using a 'for' Loop instead of 'foreach'.
// Link: https://stackoverflow.com/questions/11412956/what-is-the-best-way-of-validating-an-ip-address
bool isValid = true;
if (string.IsNullOrWhiteSpace(Text)) { isValid = false; }
// Split string by ".", check that array length is 4
string[] arrOctets = Text.Split('.');
if (arrOctets.Length != 4) { isValid = false; }
// Check Each Sub-String (Ensure that it Parses to byte)
byte obyte = 0;
for (int i = 0; i < arrOctets.Length; i++)
{
string strOctet = arrOctets[i];
if (!byte.TryParse(strOctet, out obyte)) { isValid = false; }
}
// Set Default TextBox Text if IP is Invalid:
if (!isValid) { Text_SetDefaultValue(); }
return isValid;
}
/// <summary> Calculates the Nr. of Occurrences for the Specified Char Parameter. </summary>
/// <param name="char"></param>
/// <returns> The Number of the Received Char Parameter Occurrences Found in the TextBox Text. </returns>
private int NrCharOccurrences(char @char)
{
return Text.Split(@char).Length - 1;
}
/// <summary> Adds the Currency Symbol to the End of the TextBox Text. </summary>
private void Text_AddCurrencyDesignator()
{
// Add this to Control Event: Control_Leave
if (inputType.Equals(TextBoxInputType.Currency))
{
if (!string.IsNullOrEmpty(Text) & !string.IsNullOrWhiteSpace(Text))
{
TextAlign = HorizontalAlignment.Right;
switch (designatorAlignment)
{
case DesignatorAlignment.Left:
if (!Text.StartsWith(currencyDesignator))
{
Text = $"{currencyDesignator} {Text}";
}
break;
case DesignatorAlignment.Right:
if (!Text.EndsWith(currencyDesignator))
{
Text = $"{Text} {currencyDesignator}";
}
break;
}
}
}
Text_Align();
}
/// <summary> Remove the Currency Symbol to the End of the TextBox Text. </summary>
private void Text_RemoveCurrencyDesignator()
{
if (inputType.Equals(TextBoxInputType.Currency))
{
Text = Text.Replace(currencyDesignator, string.Empty);
}
}
/// <summary> Remove White Spaces from TextBox Text. </summary>
private void Text_RemoveWhiteSpaces()
{
if (inputType.Equals(TextBoxInputType.Currency) ^ inputType.Equals(TextBoxInputType.Numeric))
{
Text = Text.Replace(" ", string.Empty);
}
}
/// <summary> Align TextBox Text. </summary>
private void Text_Align()
{
switch (inputType)
{
case TextBoxInputType.Default: TextAlign = HorizontalAlignment.Left; break;
case TextBoxInputType.Numeric:
case TextBoxInputType.Currency: TextAlign = HorizontalAlignment.Right; break;
case TextBoxInputType.IPV4: TextAlign = HorizontalAlignment.Center; break;
}
}
/// <summary> Sets the Text Value as a Decimal Value by Inserting Missing Zeros. </summary>
private void Text_SetDecimalValue()
{
if (useDecimals)
{
decimal decVal = -1;
string val = string.Empty;
// Success:
// [Reference]: if (decimal.TryParse(Text, out decVal)) { val = decVal.ToString("0.00"); }
if (decimal.TryParse(Text, out decVal)) { val = decVal.ToString(decimalFormat); }
// else { /* FAIL */ }
// Set the Decimal Value as Text
Text = val;
}
}
/// <summary> Sets the Default Text Value to Each Input Type. </summary>
private void Text_SetDefaultValue()
{
switch (inputType)
{
case TextBoxInputType.Default: Text = string.Empty; break;
case TextBoxInputType.Numeric:
if (string.IsNullOrEmpty(Text)) { Text = "0"; }
else
{
if (IsNumericString(Text)) { Text = Text; }
}
break;
case TextBoxInputType.Currency:
if (string.IsNullOrEmpty(Text)) { Text = "0"; }
else
{
if (IsNumericString(Text))
{
Text_SetDecimalValue();
Text_AddCurrencyDesignator();
Text = Text;
}
}
break;
case TextBoxInputType.IPV4: Text = "0.0.0.0"; break;
}
}
#endregion
}
}
Further References: