Home > Software design >  Textbox, WinForms, C#. Displaying a number with two digits after the decimal point while maintaining
Textbox, WinForms, C#. Displaying a number with two digits after the decimal point while maintaining

Time:12-23

Is there an easy way to have textboxes in WinForms accept numbers with many digits after the decimal point but display only the first two digits while maintaining the full number for the calculations that are done in the background?

For Example: If the user enters 3.5689 in the textbox, I want the textbox to actually contain the entire 3.5689 number but display only 3.57. Just like what can be done in the excel spreadsheet using the icons shown in the following image:

Decimal icons in Excel

The only way I could think of solving this problem is by creating a variable in the background that grabs the full number from the textbox every time the text in the textbox is changed while displaying the rounded number in the textbox every time the text is changed. However, this will require extensive modifications to my current code which I would like to avoid.

Any ideas of a simpler easier way to do that?

CodePudding user response:

The easiest approach is custom-painting the TextBox when it doesn't have focus.

In the following example, I have created a FormattedNumberTextBox control which:

  • Has a Format property. The default is N2 which formats the number with thousand separator and two decimal points. You can assign any enter image description here

    Here is the code:

    using System;
    using System.Drawing;
    using System.Windows.Forms;
    public class FormattedNumberTextBox : TextBox
    {
        const int WM_PAINT = 0xF;
        string format = "N2";
        public string Format
        {
            get { return format; }
            set
            {
                format = value;
                Invalidate();
            }
        }
        string errorText = "#ERROR";
        public string ErrorText
        {
            get { return errorText; }
            set
            {
                errorText = value;
                Invalidate();
            }
        }
        protected override void WndProc(ref Message m)
        {
            base.WndProc(ref m);
            if (m.Msg == WM_PAINT &&
                !string.IsNullOrEmpty(Format) &&
                !string.IsNullOrEmpty(Text) &&
                !Focused)
            {
                using (var g = Graphics.FromHwnd(Handle))
                {
                    var r = new Rectangle(1, 1, Width - 1, Height - 1);
                    using (var b = new SolidBrush(BackColor))
                        g.FillRectangle(b, r);
                    var fortamttedValue = ErrorText;
                    if (long.TryParse(Text, out long l))
                        try { fortamttedValue = String.Format($"{{0:{Format}}}", l); } catch { }
                    else if (double.TryParse(Text, out double d))
                        try { fortamttedValue = String.Format($"{{0:{Format}}}", d); } catch { }
                    TextRenderer.DrawText(g, fortamttedValue, Font, r,
                        ForeColor, BackColor, TextFormatFlags.TextBoxControl
                        | TextFormatFlags.NoPadding);
                }
            }
        }
    }
    

    If the flicker is too much, you may want to improve the paint or cover the text with a Label, then hide/show label when the focus changes.

    CodePudding user response:

    The only way I could think of solving this problem is by creating a variable in the background that grabs the full number from the textbox every time the text in the textbox is changed

    This is what you want to do. Remember, textbox controls only contain strings, but what you really care about is the decimal number. That's a different data type, and so you're really going to be better off taking the time to make a Decimal property for that field.

    It's also important to understand the TextChanged event on the control does not care whether the user changed the text or your code. It will fire either way. That means you're gonna need to be careful you don't go running around in circles: user makes a change, the event fires which causes your code to update the backing field, round the value, and update the textbox, which causes the event to fire again and now the backing field is also updated to the rounded value. Not good. You may want to look at LostFocus instead.

    CodePudding user response:

    Your post indicates (understandably) that you would like to avoid extensive modifications to current code. One approach that offers a minimal impact is to make a custom TextBoxF2 : TextBox class because all you would need to do is manually edit the designer file, changing TextBox references to TextBoxF2 instead.


    Behavior requirements.

    1. Accept numbers with many digits after the decimal point
    2. Display only the first two digits
    3. Maintain the full number for the calculations that are done in the background

    I focused entry

    displayed-v-underlying


    Example of a custom textbox that meets these requirements:

    class TextBoxF2 : TextBox, INotifyPropertyChanged
    {
        public decimal Value
        {
            get => _value;
            set
            {
                if (!Equals(_value, value))
                {
                    _value = value;
                    OnPropertyChanged();
                }
            }
        }
        decimal _value = 0;
        protected override void OnKeyDown(KeyEventArgs e)
        {
            base.OnKeyDown(e);
            if(e.KeyData.Equals(Keys.Enter)) 
            {
                e.SuppressKeyPress = e.Handled = true;
                validate();
                BeginInvoke(() => SelectAll());
            }
        }
        protected override void OnGotFocus(EventArgs e)
        {
            base.OnGotFocus(e);
            BeginInvoke(()=>SelectAll());
        }
        protected override void OnLostFocus(EventArgs e)
        {
            base.OnLostFocus(e);
            validate();
        }
        private void validate()
        {
            if(decimal.TryParse(Text, out decimal success))
            {
                Value = success;
            }
            else
            {
                // Revert
                onValueChanged();
            }
        }
        private void onValueChanged() => Text = Value.ToString("F2");
        public event PropertyChangedEventHandler? PropertyChanged;
        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            switch (propertyName)
            {
                case nameof(Value):
                    onValueChanged();
                    break;
            }
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
    

    There are many ways to do what you're asking. Hopefully this moves you closer to an ideal solution.

  • Related