Home > Back-end >  Displaying a number with two digits after the decimal point while maintaining the entire unrounded n
Displaying a number with two digits after the decimal point while maintaining the entire unrounded n

Time:12-30

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);
                }
            }
        }
    }
    

    Scenario for DataGridView

    For DataGridView, you can handle CellPainting to achieve the same:

    private void DataGridView1_CellPainting(object sender, DataGridViewCellPaintingEventArgs e)
    {
        if (e.ColumnIndex == 1 && e.RowIndex >= 0)
        {
            var formattedValue = "";
            if (e.Value != null && e.Value != DBNull.Value)
                formattedValue = string.Format("{0:N2}", e.Value);
            e.Paint(e.ClipBounds, DataGridViewPaintParts.All
                & ~DataGridViewPaintParts.ContentForeground);
            var selected = (e.State & DataGridViewElementStates.Selected) != 0;
            TextRenderer.DrawText(e.Graphics,
                formattedValue,
                e.CellStyle.Font,
                e.CellBounds,
                selected ? e.CellStyle.SelectionForeColor : e.CellStyle.ForeColor,
                TextFormatFlags.TextBoxControl | TextFormatFlags.VerticalCenter);
            e.Handled = true;
        }
    }
    

    Now if you copy the DataGridView values, or if you start editing the cell, the original values will be used. But for display, we paint the formatted value.

    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:

    The problem: When the TextBox control has focus, show the original value, but when the control doesn't have focus, show the formatted value.

    In addition to the above solution (painting the formatted value), as another option for the scenarios when you are using DataBinding, you can use the following solution:

    • Set the enter image description here

      Example

      Assuming you have a databinding like the following:

      textBox1.DataBindings.Add("Text", theDataSource, "TheColumnName", true);
      

      Then in the load event of the form or in the constructor, do the following setup:

      textBox1.DataBindings["Text"].FormatString = "N2";
      textBox1.Enter  = (obj, args) => textBox1.DataBindings["Text"].FormatString = "";
      textBox1.Validated  = (obj, args) => textBox1.DataBindings["Text"].FormatString = "N2";
      

      Scenario for DataGridView

      Same could be achieved for DataGridViewTextBoxColumn as well, assuming you have set the format for the cell in designer or in code like this:

      dataGridView1.Columns[0].DefaultCellStyle.Format = "N2";
      

      Then in the CellFormatting event, you can check if the cell is in edit mode, remove the format, otherwise set it to the desired format again:

      private void DataGridView1_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e)
      {
          if (e.ColumnIndex == 0 && e.RowIndex >= 0)
          {
              var cell = dataGridView1[e.ColumnIndex, e.RowIndex];
              if (cell.IsInEditMode)
                  e.CellStyle.Format = "";
              else
                  e.CellStyle.Format = "N2";
          }
      }  
      

      CodePudding user response:

      The screenshot focused entry or reentry Focused entry or re-entry.

      response to enter key Response to Enter key


      Handle the Enter key

      This method also responds to an Escape key event by reverting to the last good formatted value.

      protected override void OnKeyDown(KeyEventArgs e)
      {
          base.OnKeyDown(e);
          switch (e.KeyData)
          {
              case Keys.Return:
                  e.SuppressKeyPress = e.Handled = true;
                  OnValidating(new CancelEventArgs());
                  break;
              case Keys.Escape:
                  e.SuppressKeyPress = e.Handled = true;
                  formatValue();
                  break;
          }
      }
      

      Define behavior for when TextBox calls its built-in validation.

      This performs format SelectAll. If the new input string can't be parsed it simply reverts to the previous valid state.

      protected override void OnValidating(CancelEventArgs e)
      {
          base.OnValidating(e);
          if (Modified)
          {
              if (double.TryParse(Text, out double value))
              {
                  Value = value;
              }
              formatValue();
              _unmodified = Text;
              Modified = false;
          }
      }
      

      Ensure that a mouse click causes the full-resolution display:

      • Whether or not the control gains focus as a result.
      • Only if control is not read only.

      Use BeginInvoke which doesn't block remaining mouse events in queue.

      protected override void onm ouseDown(MouseEventArgs e)
      {
          base.OnMouseDown(e);
          if (!(ReadOnly || Modified))
          {
              BeginInvoke(() =>
              {
                  int selB4 = SelectionStart;
                  Text = Value == 0 ? "0.00" : $"{Value}";
                  Modified = true;
                  Select(Math.Min(selB4, Text.Length - 1), 0);
              });
          }
      }
      

      Implement the bindable Value property for the underlying value

      Allows setting the underlying value programmatically using textBoxFormatted.Value = 123.456789.

      class TextBoxFP : TextBox, INotifyPropertyChanged
      {
          public TextBoxFP()
          {
              _unmodified = Text = "0.00";
              CausesValidation = true;
          }
          public double Value  
          {
              get => _value;
              set
              {
                  if (!Equals(_value, value))
                  {
                      _value = value;
                      formatValue();
                      OnPropertyChanged();
                  }
              }
          }
          double _value = 0;    
          public event PropertyChangedEventHandler? PropertyChanged;
          protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
          {
              PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
          }
      }
      

      Manage the built-in Modified property of the text box and the actual formatting.

      string _unmodified;
      protected override void OnTextChanged(EventArgs e)
      {
          base.OnTextChanged(e);
          if(Focused)
          {
              Modified = !Text.Equals(_unmodified);
          }
      }
      public string Format { get; set; } = "N2";
      private void formatValue()
      {
          Text = Value.ToString(Format);
          Modified = false;
          BeginInvoke(() => SelectAll());
      }
      

      testbench

  • Related