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:
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 isN2
which formats the number with thousand separator and two decimal points. You can assign anyHere 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 atLostFocus
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, changingTextBox
references toTextBoxF2
instead.
Behavior requirements.
- Accept numbers with many digits after the decimal point
- Display only the first two digits
- Maintain the full number for the calculations that are done in the background
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.