2

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?

Joel Coehoorn
  • 399,467
  • 113
  • 570
  • 794
  • Consider using `MaskedTextBox` instead - it's designed for this purpose. Your can specify a mask (a format) to use when displaying your values. It might not round, it might truncate (I don't recall) but that's the easiest way to accomplish the result you're looking for. – CoolBots Dec 22 '22 at 18:46

5 Answers5

1

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 standard numeric format or any custom numeric format to this property.
  • Has an ErrorText property. The default value is #ERROR which will be shown when there's a format error.
  • Shows the original value when the control has focus and shows the formatted value when it's not focused.

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.

Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
  • The approach is similar to rendering the Placeholder text. The idea has been used here in [this post](https://stackoverflow.com/a/36534068/3110834) and in [WinForms](https://github.com/dotnet/winforms/blob/106bd36fce0e5fafefdd56bc749c4e525459b7ec/src/System.Windows.Forms/src/System/Windows/Forms/TextBox.cs#L893). – Reza Aghaei Dec 23 '22 at 12:39
  • Another idea could be covering the text with a `Label`, then hide/show label when the focus changes. – Reza Aghaei Dec 23 '22 at 12:41
  • Trying to modify the Text property (by parsing and formatting) may introduce some complexities in case of data-biding. – Reza Aghaei Dec 23 '22 at 12:43
  • Thanks brother. Thats really neat. It worked like a charm. Is it possible to achieve something similar for a DataGridViewTextBoxColumn? – user20593243 Dec 24 '22 at 02:08
  • @user20593243 If it's not the default behavior when setting format of the cell, then it should be possible in formatting or paint events. Do a quick research, give it a try and feel free to ask a new question. I'll share my idea if I have any :) – Reza Aghaei Dec 24 '22 at 12:22
  • @user20593243 you can use the `DefaultCellStyle.Format` property of a `DataGridViewColumn` to do that here's an [example](https://github.com/IVSoftware/data-grid-view-format-column-00/blob/master/data-grid-view-format-column/MainForm.cs). – IVSoftware Dec 25 '22 at 19:36
  • @IVSoftware I have tried this but it seems it displays the rounded number without retaining the original. I am looking for something similar to what this code achieves. i.e. displays the rounded number but if the user wants to copy the contents of the DataGridViewColumn and paste them say to an excel or a text file they would actually get the original numbers. I am not sure if that's achievable or not but anyways this issue is not really that important. If it becomes important for me in the future I will post a new question with details of what I want and what I tried. – user20593243 Dec 26 '22 at 22:56
  • @user20593243 I gave it a try and it's achievable and rather straightforward, using the CellFormatting event. What you need to do, is similar to the idea that I've posted in my other answer; first set the format string for the default cell style of the column, then in the CellFormatting event, check if the cell is in edit mode, remove the format string of the default cell style, else, set it to desired format. – Reza Aghaei Dec 27 '22 at 00:03
  • By the way, if you are using databinding, you may find my other answer also useful. – Reza Aghaei Dec 27 '22 at 00:03
  • I assure you that the underlying value is intact. You can hover over the cell and see full resolution in the tooltip per this [image](https://github.com/IVSoftware/data-grid-view-format-column-00). But it's true that a copy-paste operation is going to take what's visible in the cell at the moment... – IVSoftware Dec 27 '22 at 00:24
  • @IVSoftware Well, it's intact, but as soon as user starts editing, it just loads the rounded value, so the user doesn't see the original value. As I mentioned in my other comment, you need to set and reset the format in CellFormatting event to make sure user can see the original value once they start editing. – Reza Aghaei Dec 27 '22 at 00:44
  • Updated the other answer with DataGridView scenario. – Reza Aghaei Dec 27 '22 at 00:51
  • @RezaAghaei I assure you that the user _does_ see the original value when editing begins. Make sure you're evaluating the latest [commit](https://github.com/IVSoftware?tab=repositories). – IVSoftware Dec 27 '22 at 00:56
  • @IVSoftware Last time I saw it yesterday, where it was not setting the original value in EditingControlShowing event handler. Now I it should work. – Reza Aghaei Dec 27 '22 at 01:03
  • @IVSoftware Thanks. Now I can see it is working but there is still the lack of the ability to copy-paste the entire grid with its original values. – user20593243 Dec 27 '22 at 03:38
  • @RezaAghaei There is no databinding in my current project but it should be useful for the future. Thanks. – user20593243 Dec 27 '22 at 03:39
  • @user20593243 latest [commit](https://github.com/IVSoftware/data-grid-view-format-column-00.git) adds **basic copy-paste entire grid with original values**. – IVSoftware Dec 28 '22 at 23:06
1

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.

Joel Coehoorn
  • 399,467
  • 113
  • 570
  • 794
1

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:

The you can see the result:

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";
    }
}  
Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
  • Thanks. That was quite straight forward for the DataGridViewTextBoxColumn but I still would like to be able to copy-paste the entire grid with its original values. That's actually more useful for me than being able to see the original number while in edit mode. – user20593243 Dec 27 '22 at 03:43
  • @user20593243 Thanks for the feedback. Then you need to rely on `Paint` event. This way, the value that you get when you Copy is the original value. – Reza Aghaei Dec 27 '22 at 04:33
  • @user20593243 Paint solution for DataGridView added to the other answer. – Reza Aghaei Dec 27 '22 at 04:57
  • Fantastic. You rock brother. Thanks a lot. – user20593243 Dec 27 '22 at 18:29
0

The accepted answer is simple and elegant. It states that it's the "easiest" way and I agree!

But as I understand it, the FormattedNumberTextBox scheme as coded today relies on changes to the focused state of the control so there's at least one "possible" issue: if the user types some keys then hits Enter key there might not be a change of focus (by design or default).

So, this post just makes a few tweaks to an already excellent answer by handling [Enter] and adding another nice amenity - a settable/bindable Value property that fires PropertyChanged events when a new valid value is received (either by keyboard input or programmatically). At the same time, it ensures that a textbox that is ReadOnly will always display the formatted value.

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

Testing

Bind the PropertyChanged event and attempt various valid (1.2345) and invalid ("abc") entries.

public partial class MainForm : Form
{
    public MainForm()
    {
        InitializeComponent();
        textBoxFormatted.PropertyChanged += (sender, e) =>
        {
            if(e.PropertyName == nameof(TextBoxFP.Value))
            {
                textBoxBulk.Value = textBoxFormatted.Value * 100;
                textBoxDiscount.Value = textBoxBulk.Value * - 0.10;
                textBoxNet.Value = textBoxBulk.Value + textBoxDiscount.Value;
            }
        };
        buttonTestValue.Click += (sender, e) => textBoxFormatted.Value = (double)Math.PI;
    }
}

testbench

IVSoftware
  • 5,732
  • 2
  • 12
  • 23
0

This Code is working...

private void txtAmount_TextChanged(object sender, EventArgs e)
    {
        if (txtAmount.Text.Length > 0)
        {
            dblGroupedNumber = Convert.ToDouble(txtAmount.Text);
            txtAmount.Text = string.Format("{0:n0}", dblGroupedNumber);
        }
    }