Note: If you like this article, please vote for it!
Introduction
TNumEditBox
is a free and open source numeric edit control in C# applications, which has the following features:
- Can set decimal length (0 denotes integer, maximum decimal length can be 10)
- Can directly get the integer value or decimal value of Text property
- Can keep
BackColor
when control is ReadOnly - Can allow negative input or not
- Allowed maximum numeric Text length is 28
- Supports mouse Cut, Copy, Paste, Clear in context menu
- Supports shortcut keys Ctrl+X, Ctrl+C or Ctrl+V
Background
There are already many open source controls for numeric edit, but most of which handle keyboard input only, few support mouse Cut, Copy, Paste and Clear in context menu, few process conventional shortcut keys such as Ctrl+X, Ctrl+C or Ctrl+V too.
TNumEditBox
is a C# custom TextBox
control for numeric edit. It references the famous freeware Delphi control PBNumEdit in PBEditPack, and part of its code lines are translated from Object Pascal in PBNumEdit.pas. But, there are some obvious differences in C# and Delphi:
PBNumEdit
need not handle shortcut keys, it only overrides WMCut
, WMPaste
and WMCopy
procedures.TNumEditBox
must handle Ctrl+C and Ctrl+V in overridden ProcessCmdKey()
.TNumEditBox
must handle WM_CUT
, WM_COPY
, WM_PASTE
, WM_CLEAR
messages instead of override as PBNumEdit
does.
Key Points
Text edit box usually considers two input cases: one is keyboard input, the other is mouse operations, which correspond to TNumEditBox
's two processing contents:
- Override key input process method or event such as
ProcessCmdKey
, OnKeyDown
, OnKeyPress
- Override message process method
WndProc()
for mouse operations in context menu
Another important task in TNumEditBox
is how to calculate the new this.SelectionStart
value when char delete or text input, particularly in cases of shortcut keys and mouse operations.
1) Override OnKeyDown Event
The first key point is that TNumEditBox
overrides OnKeyDown
event to handle Delete key or BackSpace key, and clears selected text by ClearSelection()
or deletes one char by DeleteText()
.
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (!this.ReadOnly)
{
if(e.KeyData == Keys.Delete || e.KeyData == Keys.Back)
{
if(this.SelectionLength > 0)
{
this.ClearSelection();
}
else
{
this.DeleteText(e.KeyData);
}
e.SuppressKeyPress = true;
}
}
}
2) Handle Shortcut Keys
The commonly used shortcut keys include Ctrl+X, Ctrl+C and Ctrl+V. In .NET 2.0, the shortcut Ctrl+X can be captured by message WM_CUT
in context menu, so only Ctrl+C and Ctrl+V need be considered in overridden ProcessCmdKey()
.
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
if (keyData == (Keys)Shortcut.CtrlV )
{
this.ClearSelection();
string text = Clipboard.GetText();
for (int k = 0; k < text.Length; k++)
{
SendCharKey(text[k]);
}
return true;
}
else if (keyData == (Keys)Shortcut.CtrlC)
{
Clipboard.SetText(this.SelectedText);
return true;
}
return base.ProcessCmdKey(ref msg, keyData);
}
private void SendCharKey(char c)
{
Message msg = new Message();
msg.HWnd = this.Handle;
msg.Msg = WM_CHAR;
msg.WParam = (IntPtr)c;
msg.LParam = IntPtr.Zero;
base.WndProc(ref msg);
}
Method SendCharKey()
imitates to input clipboard text whose skill is firing WndProc()
to send key input message, since the .NET 2.0 static
method SendKeys.Send()
fails in ProcessCmdKey()
but works well in overridden WndProc()
!
3) Handle Context Menu Messages
In .NET 2.0, the four operations in context menu: Cut, Copy, Paste, Clear correspond to four Windows messages and their values: WM_CUT(0x0300)
, WM_COPY(0x0301)
, WM_PASTE(0x0302)
, WM_CLEAR(0x0303)
. The usual method to disable context menu in control is that it creates a ContextMenu
object with no item in constructor as open source control BANumEdit does:
public TNumEditBox()
{
this.ContextMenu = new ContextMenu();
}
TNumEditBox
captures context menu messages by overriding WndProc()
:
protected override void WndProc(ref Message m)
{
if (m.Msg == WM_PASTE)
{
this.ClearSelection();
SendKeys.Send(Clipboard.GetText());
base.OnTextChanged(EventArgs.Empty);
}
else if (m.Msg == WM_COPY)
{
Clipboard.SetText(this.SelectedText);
}
else if (m.Msg == WM_CUT)
{
Clipboard.SetText(this.SelectedText);
this.ClearSelection();
base.OnTextChanged(EventArgs.Empty);
}
else if (m.Msg == WM_CLEAR)
{
this.ClearSelection();
base.OnTextChanged(EventArgs.Empty);
}
else
{
base.WndProc(ref m);
}
}
Here SendKeys.Send()
imitates keyboard input in order to fire OnKeyPress
event which handles input, calculates the SelectionStart
, etc.
4) Override OnKeyPress Event
In TNumEditBox
, all input operations such as keyboard or mouse (context menu) will be handled through OnKeyPress
event whose aim includes:
- Prevent invalid char input
- Insert or delete one char
- Calculate
new this.SelectionStart
value
protected override void OnKeyPress(KeyPressEventArgs e)
{
base.OnKeyPress(e);
if (this.ReadOnly)
{
return;
}
if (e.KeyChar == (char)13 || e.KeyChar == (char)3 ||
e.KeyChar == (char)22 || e.KeyChar == (char)24)
{
return;
}
if (m_decimalLength == 0 && e.KeyChar == m_decimalSeparator)
{
e.Handled = true;
return;
}
if (!m_allowNegative && e.KeyChar == m_negativeSign &&
base.Text.IndexOf(m_negativeSign) < 0)
{
e.Handled = true;
return;
}
if (!char.IsDigit(e.KeyChar) && e.KeyChar != m_negativeSign &&
e.KeyChar != m_decimalSeparator)
{
e.Handled = true;
return;
}
if (base.Text.Length >= m_MaxValueLength && e.KeyChar != m_negativeSign)
{
e.Handled = true;
return;
}
if (e.KeyChar == m_decimalSeparator)
{
this.SelectionLength = 0;
}
else
{
this.ClearSelection();
}
bool isNegative = (base.Text[0] == m_negativeSign) ? true : false;
if (isNegative && this.SelectionStart == 0)
{
this.SelectionStart = 1;
}
if (e.KeyChar == m_negativeSign)
{
int selStart = this.SelectionStart;
if (!isNegative)
{
base.Text = m_negativeSign + base.Text;
this.SelectionStart = selStart + 1;
}
else
{
base.Text = base.Text.Substring(1, base.Text.Length - 1);
if (selStart >= 1)
{
this.SelectionStart = selStart - 1;
}
else
{
this.SelectionStart = 0;
}
}
e.Handled = true;
return;
}
int dotPos = base.Text.IndexOf(m_decimalSeparator) + 1;
if (e.KeyChar == m_decimalSeparator)
{
if (dotPos > 0)
{
this.SelectionStart = dotPos;
}
e.Handled = true;
return;
}
if (base.Text == "0")
{
this.SelectionStart = 0;
this.SelectionLength = 1;
}
else if (base.Text == m_negativeSign + "0")
{
this.SelectionStart = 1;
this.SelectionLength = 1;
}
else if (m_decimalLength > 0)
{
if (base.Text[0] == '0' && dotPos == 2 && this.SelectionStart <= 1)
{
this.SelectionStart = 0;
this.SelectionLength = 1;
}
else if (base.Text.Substring(0, 2) == m_negativeSign + "0" &&
dotPos == 3 && this.SelectionStart <= 2)
{
this.SelectionStart = 1;
this.SelectionLength = 1;
}
else if (this.SelectionStart == dotPos + m_decimalLength)
{
e.Handled = true;
}
else if (this.SelectionStart >= dotPos)
{
this.SelectionLength = 1;
}
else if (this.SelectionStart < dotPos - 1)
{
this.SelectionLength = 0;
}
}
}
It must point out when you see OnKeyPress
code that:
- Returns directly if control is ReadOnly
- Cannot set
e.Handled
value when some special key input such as keyvalue 3,13,22,24, etc. - Let
e.Handled = true
when invalid char input - Let
e.Handled = true
when dot(.) or minus(-) input - Let
e.Handled = true
when input at last position and DecimalLength
is bigger than 0
5) Method ClearSelection
This method is used to clear selected text, i.e., this.SelectedText
. First, it calculates the new this.SelectionStart
, then it calls DeleteText()
to delete char one by one by looping through the BackSpace key input.
private void ClearSelection()
{
if (this.SelectionLength == 0)
{
return;
}
if (this.SelectedText.Length == base.Text.Length)
{
base.Text = 0.ToString(m_valueFormatStr);
return;
}
int selLength = this.SelectedText.Length;
if (this.SelectedText.IndexOf(m_decimalSeparator) >= 0)
{
selLength--;
}
this.SelectionStart += this.SelectedText.Length;
this.SelectionLength = 0;
for (int k = 1; k <= selLength; k++)
{
this.DeleteText(Keys.Back);
}
}
6) Method DeleteText
This method is used to delete one char by Delete key or BackSpace key. The core skill is to change Delete key to BackSpace key through adding 1
to its property this.SelectionStart
, and handle uniformly the Delete key and BackSpace key. The function of DeleteText()
is:
- Delete one char by BackSpace
- Calculate
new this.SelectionStart
- Append
0
when it deletes char after dot - Handle minus when it deletes the second char
- Handle the case when
base.Text.Length = 1
private void DeleteText(Keys key)
{
int selStart = this.SelectionStart;
if (key == Keys.Delete)
{
selStart += 1;
if (selStart > base.Text.Length)
{
return;
}
if (this.IsSeparator(selStart - 1))
{
selStart++;
}
}
else
{
if (selStart == 0)
{
return;
}
if (this.IsSeparator(selStart - 1))
{
selStart--;
}
}
if (selStart == 0 || selStart > base.Text.Length)
{
return;
}
int dotPos = base.Text.IndexOf(m_decimalSeparator);
bool isNegative = (base.Text.IndexOf(m_negativeSign) >= 0) ? true : false;
if (selStart > dotPos && dotPos >= 0)
{
base.Text = base.Text.Substring(0, selStart - 1) +
base.Text.Substring(selStart, base.Text.Length - selStart) + "0";
base.SelectionStart = selStart - 1;
}
else
{
if (selStart == 1 && isNegative)
{
if (base.Text.Length == 1)
{
base.Text = "0";
}
else if (dotPos == 1)
{
base.Text = "0" + base.Text.Substring(1, base.Text.Length - 1);
}
else
{
base.Text = base.Text.Substring(1, base.Text.Length - 1);
}
base.SelectionStart = 0;
}
else if (selStart == 1 && (dotPos == 1 || base.Text.Length == 1))
{
base.Text = "0" + base.Text.Substring(1, base.Text.Length - 1);
base.SelectionStart = 1;
}
else if (isNegative && selStart == 2 && base.Text.Length == 2)
{
base.Text = m_negativeSign + "0";
base.SelectionStart = 1;
}
else if (isNegative && selStart == 2 && dotPos == 2)
{
base.Text = m_negativeSign + "0" +
base.Text.Substring(2, base.Text.Length - 2);
base.SelectionStart = 1;
}
else
{
base.Text = base.Text.Substring(0, selStart - 1) +
base.Text.Substring(selStart, base.Text.Length - selStart);
base.SelectionStart = selStart - 1;
}
}
}
Using the Code
After unzipping file TNumEditBox.zip, we can double click the solution file TNumEditBox_Demo.sln to view the demo if we have Visual Studio 2005/2008, or we can run TNumEditBox_Demo.exe in subfolder \bin\ to test the control.
There are two classes in TNumEditBox.cs: one is TTextBoxEx
, another is TNumEditBox
. The former is a TextBox
control and is the base class of TNumEditBox
. TTextBoxEx
has only one public
property KeepBackColorWhenReadOnly
used to keep BackColor
when the control is ReadOnly
. TNumEditbox
has some public
properties:
AllowNegative
: If it is true
, can show and input negative numberDecimalLength
: If it is bigger than 0
, can show and input decimal numberValue
: Get the decimal value of Text
propertyIntValue
: Get the integer value of Text
propertyKeepBackColorWhenReadOnly
: If it is true
, the BackColor
appears when it is ReadOnly
Three usages must be noticed:
- It positions after dot if
DecimalLength
is bigger than 0
and we input dot - It appends
0
at tail if we delete digit after dot - It takes override (replace) pattern when we input digit after dot, and takes insert pattern when we input digit before dot
Conclusion
TNumEditBox
is our attempt to rewrite freeware Delphi code with C# language, it takes us about ten days to do this work. We consider some cases and compare them below:
- Shows "
-0
" when we delete 1
and base.Text = "-1"
, and PBNumEdit
shows "-
" - Shows "
0.*
" when we delete -
and base.Text = "-.*"
, and PBNumEdit
shows ".*
" - Shows "
-0.*
" when we delete 1
and base.Text = "-1.*"
, PBNumEdit
show "-.*
"
Although TNumEditBox
does not behave the same as PBNumEdit
, for example it cannot show thousands separator, etc. however we can use it in common C# applications, and we will modify it of course.
There are a lot of freeware Delphi controls well used for many years which have no C# versions. Now, we must consider these problems:
- How to reuse them?
- How to rewrite them?
- Redesign them or rewrite instead?
History
- ver 1.0, 17th September, 2007
- ver 1.1, 29th September, 2008
- ver 1.2, 23rd October, 2008
- Fixed a bug: When input 101 it displays 11, and input -101.1 it displays -11.1, etc.
- Consider the culture number format
- ver 1.3, 24th November, 2008
- Keep the content instead of clearing it when input minus, like the dot in .
College teacher and free programmer who expertises application sofwares for statistics reports,finance data handling,and MIS using Visual C#, Delphi, SQL, etc..