Click here to Skip to main content
Email Password   helpLost your password?

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:

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:

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:

  1. Override key input process method or event such as ProcessCmdKey, OnKeyDown, OnKeyPress
  2. 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();  // clear first this.SelectedText
            }
            else
            {
                // delete char and recalculate this.SelectionStart
                this.DeleteText(e.KeyData);

            }
            // does not transform event to KeyPress, but to KeyUp
            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++) // cannot use SendKeys.Send
        {
            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)  // mouse paste
    {
        this.ClearSelection();
        SendKeys.Send(Clipboard.GetText());
        base.OnTextChanged(EventArgs.Empty);
    }
    else if (m.Msg == WM_COPY)  // mouse copy
    {
        Clipboard.SetText(this.SelectedText);
    }
    else if (m.Msg == WM_CUT)  // mouse cut or ctrl+x shortcut
    {
        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:

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)  // will position after dot(.)
    {
        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;  // minus(-) has been handled
        return;
    }

    int dotPos = base.Text.IndexOf(m_decimalSeparator) + 1;

    if (e.KeyChar == m_decimalSeparator)
    {
        if (dotPos > 0)
        {
            this.SelectionStart = dotPos;
        }
        e.Handled = true;  // dot has been handled
        return;
    }

    if (base.Text == "0")
    {
        this.SelectionStart = 0;
        this.SelectionLength = 1;  // replace the first char, i.e. 0
    }
    else if (base.Text == m_negativeSign + "0")
    {
        this.SelectionStart = 1;
        this.SelectionLength = 1;  // replace the first char, i.e. 0
    }
    else if (m_decimalLength > 0)
    {
        if (base.Text[0] == '0' && dotPos == 2 && this.SelectionStart <= 1)
        {
            this.SelectionStart = 0;
            this.SelectionLength = 1;  // replace the first char, i.e. 0
        }
        else if (base.Text.Substring(0, 2) == m_negativeSign + "0" &&
				dotPos == 3 && this.SelectionStart <= 2)
        {
            this.SelectionStart = 1;
            this.SelectionLength = 1;  // replace the first char, i.e. 0
        }
        else if (this.SelectionStart == dotPos + m_decimalLength)
        {
            e.Handled = true;  // last position after text
        }
        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:

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--; // selected text contains dot(.), selected length minus 1
    }

    this.SelectionStart += this.SelectedText.Length;  // after selected text
    this.SelectionLength = 0;

    for (int k = 1; k <= selLength; k++)
    {
        this.DeleteText(Keys.Back);  // delete char one by one
    }
}

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:

private void DeleteText(Keys key)
{
    int selStart = this.SelectionStart;  // base.Text will be delete at selStart - 1

    if (key == Keys.Delete) // Delete key change to BackSpace key,
			// adjust selStart value
    {
        selStart += 1;  // adjust position for BackSpace
        if (selStart > base.Text.Length)  // text end
        {
            return;
        }

        if (this.IsSeparator(selStart - 1))  // next if delete dot(.) or thousands(;)
        {
            selStart++;
        }
    }
    else  // BackSpace key
    {
        if (selStart == 0)  // first position
        {
            return;
        }

        if (this.IsSeparator(selStart - 1)) // char which will be delete is separator
        {
            selStart--;
        }
    }

    if (selStart == 0 || selStart > base.Text.Length)  // selStart - 1 no digit
    {
        return;
    }

    int dotPos = base.Text.IndexOf(m_decimalSeparator);
    bool isNegative = (base.Text.IndexOf(m_negativeSign) >= 0) ? true : false;

    if (selStart > dotPos && dotPos >= 0)  // delete digit after dot(.)
    {
        base.Text = base.Text.Substring(0, selStart - 1) +
		base.Text.Substring(selStart, base.Text.Length - selStart) + "0";
        base.SelectionStart = selStart - 1;  // SelectionStart is unchanged
    }
    else // delete digit before dot(.)
    {
        // delete 1st digit and Text is negative, i.e.. delete minus(-)
        if (selStart == 1 && isNegative)

        {
            if (base.Text.Length == 1)  // i.e. base.Text is '-'
            {
                base.Text = "0";
            }
            else if (dotPos == 1)  // -.* format
            {
                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;
        }
        // delete 1st digit before dot(.) or Text.Length = 1
        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)  // -* format
        {
            base.Text = m_negativeSign + "0";
            base.SelectionStart = 1;
        }
        else if (isNegative && selStart == 2 && dotPos == 2)  // -*.* format
        {
            base.Text = m_negativeSign + "0" +
			base.Text.Substring(2, base.Text.Length - 2);
            base.SelectionStart = 1;
        }
        else  // selStart > 0
        {
            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:

Three usages must be noticed:

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:

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:

History

You must Sign In to use this message board.
 
 
Per page   
 FirstPrevNext
QuestionSame in DataGridView
angabanga
6:53 30 Sep '08  
Great article. Is it possible to have input control in DataGridView cells?

Thanks.
AnswerRe: Same in DataGridView
HU Lihui
3:33 1 Oct '08  
The key problem is how to custom TNumEditBox into DataGridView.I do not try it, but I think it is very useful in DataGridView application.There are many skills to custom TextBox into DataGridView,so I think that we can replace customized TextBox with TNumEditBox.If you find a way, please post an article to me.
Thanks.

Think out the box.

GeneralWhat about currency?
devnet247
9:35 29 Sep '08  
Hi,
Thanks for the code.
Just wondering you could add handling currencies depending on culture.
adding the "£" sign or Euro Sign etc...

thanks again

thanks a lot

GeneralRe: What about currency?
HU Lihui
16:57 29 Sep '08  
Thanks for your suggestion.
I realized it at my first version 1.0 like the Delphi PBNumEdit does,but I found that it shows incorrect format in mouse contextmenu.I plan to add the thounsand seperator format in my next version.I think it may be easy to add currency notation than to show thousand seperator.

Think out the box.

GeneralRe: What about currency?
TobiasP
1:03 12 Oct '08  
I noticed that the code currently looks for a dot when checking if a number is valid, but a dot is not always the separator between the integer and decimal part of a number: It is culture dependent, e.g. in my native language Swedish we use a comma instead of a dot as decimal separator. The same goes for the negative sign, thousand separator, the placement of the currency symbol and of course the currency symbol itself, they all depend on the current culture. I suggest you look at the NumberFormat property of the current culture (Thread.CurrentThread.CurrentCulture) and, as a first simple step towards internationalization, check for the cultures negative sign and decimal separator instead of the minus char and dot char. Apart from that, it seems to be a useful control.
GeneralRe: What about currency?
HU Lihui
16:17 12 Oct '08  
Thanks for your good and right idea.There is a bug when TNumEditBox used in some countries, and I will consider the number culture problem in my next version.

Think out the box.


Last Updated 26 Nov 2008 | Advertise | Privacy | Terms of Use | Copyright © CodeProject, 1999-2010