A WPF Numeric Entry Control with Pop-up Calculator

Part 1 – WPF Numeric Entry Control

Part 2 – This article

Download Source Code

When WPF first shipped, there was a noticeable lack of certain controls we’ve become used to in Win32 and WinForms: Calendar, DateTimePicker, and NumericUpDown. WPF 4 adds Calendar and DatePicker, but not anything for numeric entry.

I described how to create a numeric entry control in a previous article. In this article, I will follow that up by adding a popup calculator to allow you to quickly manipulate the number.

If you find this article useful, please consider buying my best-selling book, Writing High-Performance .NET Code.

Adding a Popup Calculator

Now that we have a basic numeric entry control, let’s extend it by adding the power of a keyboard-driven calculator popup window.

If you’ve used Quicken or the now-sadly-defunct Microsoft Money, you’ve seen a feature that pops up a calculator window whenever you hit +, –, *, or / in a number field.

Here are some further requirements for such a control:

  • It should be activated when the user types +, –, *, or /
  • It should be dismissed if the user hits Escape, Enter, or clicks anywhere outside the calculator (which is considered equivalent to Enter, not escape)
  • Both mouse and keyboard input should be handled
  • This version only handles integers
  • It should handle Backspace key to erase previously-input character
  • It should be visually compact
  • It should allow the user to string computations together. A + B – C / D * E

Extend the Numeric Entry Control

We first need to come up with a way to trigger the calculator in our numeric entry control. This is going to simply be if the user hits any math-operation character, such as +, –, *, or /.

First, we’ll need to define the allowed operations and a way to associate keyboard keys with math operations:

public enum MathOperation
{
    None = 0,
    Add,
    Subtract,
    Multiply,
    Divide
};

Now, let’s subclass NumericEntryControl with a new class that adds some additional keyboard handling.

class NumericEntryWithCalcControl : NumericEntryControl
{
    static Dictionary<Key, MathOperation> _keyToOp =
        new Dictionary<Key, MathOperation>();

    static NumericEntryWithCalcControl()
    {
        //associate keyboard keys with a math operation
        _keyToOp[Key.Add] = MathOperation.Add;
        _keyToOp[Key.Subtract] = MathOperation.Subtract;
        _keyToOp[Key.Multiply] = MathOperation.Multiply;
        _keyToOp[Key.Divide] = MathOperation.Divide;

#if DEBUG
        //easier keys on a laptop
        _keyToOp[Key.A] = MathOperation.Add;
        _keyToOp[Key.S] = MathOperation.Subtract;
        _keyToOp[Key.M] = MathOperation.Multiply;
        _keyToOp[Key.D] = MathOperation.Divide;
#endif
    }
}

With this bookkeeping down, we can intercept the keystrokes and show our calculator window (which we’ll define later) in the right spot. We also should handle when the window closes.

private CalcWindow _calc = null;

protected override void OnKeyDown(KeyEventArgs e)
{
    MathOperation op = MathOperation.None;
    if (_keyToOp.TryGetValue(e.Key, out op))
    {
        e.Handled = true;
        HandleOperation(op);
    }
    else
    {
        e.Handled = false;
        base.OnKeyDown(e);
    }
}

private void HandleOperation(MathOperation mathOperation)
{
    //show the calc dialog and initialize with Value and Op
    if (mathOperation != MathOperation.None)
    {
        _calc = new CalcWindow(Value, mathOperation);
        _calc.Closed += new EventHandler(calc_Closed);
        Point point = this.PointToScreen(new Point(0, 0));

        //adjust location to correct for DPI settings
        PresentationSource source = PresentationSource.FromVisual(this);

        double dpiX = 96.0 * source.CompositionTarget.TransformToDevice.M11;
        double dpiY = 96.0 * source.CompositionTarget.TransformToDevice.M22;

        _calc.Left = point.X * 96.0 / dpiX;
        _calc.Top = point.Y * 96.0 / dpiY;

        _calc.Show();
    }
}

private void calc_Closed(object sender, EventArgs e)
{
    if (_calc != null)
    {
        this.Value = _calc.CurrentValue;
        _calc.Closed -= new EventHandler(calc_Closed);
        _calc = null;
    }
}

Notice in ProcessOperation that the location of the calculator window is set to the upper left corner of the NumericEntryControl and it takes into account the current DPI. This is important if you’re using anything other than the standard 96 DPI.

Create the Calculator

calc_demo2

The actual calculator is a WPF window with no border (or a thin one), a textbox, and buttons representing a standard calculator layout.

The screenshot to the right shows the design layout of this window. The C button clears the input and sets the current value to 0. The left arrow erases the last character you input.

Since this window is designed to popup over other controls, I wanted to keep it as compact as possible.

The XAML code looks like this:

<Window x:Class="NumericEntryDemo.CalcWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:my="clr-namespace:NumericEntryDemo"
        Title="CalcWindow" WindowStyle="None"
        SizeToContent="WidthAndHeight"
        WindowStartupLocation="Manual"
        ResizeMode="NoResize"
        BorderThickness="1"
        >
    <DockPanel>
        <TextBox Name="_textbox"
                 DockPanel.Dock="Top"
                 Margin="2"
                 IsReadOnly="True"
                 Text="{Binding RelativeSource={RelativeSource FindAncestor,
                    AncestorType=my:CalcWindow, AncestorLevel=1}, Path=CurrentValue}"
                 TextAlignment="Right" />
        <UniformGrid Columns="4" Rows="5" Width="100" Height="100" Margin="2">
            <Button Name="_button7" Content="7" Click="digitButton_Click"/>
            <Button Name="_button8" Content="8" Click="digitButton_Click"/>
            <Button Name="_button9" Content="9" Click="digitButton_Click"/>
            <Button Name="_buttonDivide" Content="÷"  Click="opButton_Click"/>
            <Button Name="_button4" Content="4" Click="digitButton_Click"/>
            <Button Name="_button5" Content="5" Click="digitButton_Click"/>
            <Button Name="_button6" Content="6" Click="digitButton_Click"/>
            <Button Name="_buttonMultiply" Content="×"  Click="opButton_Click"/>
            <Button Name="_button1" Content="1" Click="digitButton_Click"/>
            <Button Name="_button2" Content="2" Click="digitButton_Click"/>
            <Button Name="_button3" Content="3" Click="digitButton_Click"/>
            <Button Name="_buttonSubtract" Content="-" Click="opButton_Click"/>
            <Button Name="_button0" Content="0" Click="digitButton_Click"/>
            <!-- empty space -->
            <Rectangle/>
            <Rectangle/>
            <Button Name="_buttonAdd" Content="+" Click="opButton_Click"/>
            <Button Name="_buttonClear" Content="C" Click="clearButton_click"/>
            <Button Name="_buttonErase" Content="?" Click="backButton_Click"/>
            <Button Name="_buttonEquals" Content="=" Click="equalsButton_Click"/>
        </UniformGrid>
    </DockPanel>
    <Window.Background>
        <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
            <GradientStop Color="White" Offset="0" />
            <GradientStop Color="#FFDBDBDB" Offset="1" />
        </LinearGradientBrush>
    </Window.Background>
</Window>

All of the click handlers and binding targets will be filled out in the code-behind.

To start with, we need a few housekeeping variables:

  • The original value upon starting the calculator, just in case after all the calculations the user just hits Escape
  • A way to track whether we’ve canceled the whole thing
  • The current operation
  • The current input, in both string and number form
  • The previously calculated value, allowing you to string together calculations like “123 + 456 / 2 * 29”
  • Map buttons and keystrokes to numbers, math operations, and the other commands.

With that, we can see the beginning of our class:

public partial class CalcWindow : Window
{
    private int _initialValue = 0;
    private bool _canceled = false;
    private MathOperation _currentOp = MathOperation.None;

    //value of current input
    public static readonly DependencyProperty CurrentValueProperty =
        DependencyProperty.Register("CurrentValue", typeof(int),
        typeof(CalcWindow), new PropertyMetadata(0));

    //current input in string form
    public static readonly DependencyProperty CurrentInputProperty =
        DependencyProperty.Register("CurrentInput", typeof(string),
        typeof(CalcWindow), new PropertyMetadata(""));

    //previously calculated value
    public static readonly DependencyProperty PreviousValueProperty =
        DependencyProperty.Register("PreviousValue", typeof(int),
        typeof(CalcWindow), new PropertyMetadata(0));

    private static readonly Dictionary<Key, int> _keyDigits =
        new Dictionary<Key, int>();
    private static readonly Dictionary<Key, MathOperation> _keyOps =
        new Dictionary<Key, MathOperation>();

    private readonly Dictionary<Button, int> _buttonDigits =
        new Dictionary<Button, int>();
    private readonly Dictionary<Button, MathOperation> _buttonOps =
        new Dictionary<Button, MathOperation>();

    static CalcWindow()
    {
        _keyDigits[Key.D0] = 0;
        _keyDigits[Key.D1] = 1;
        _keyDigits[Key.D2] = 2;
        _keyDigits[Key.D3] = 3;
        _keyDigits[Key.D4] = 4;
        _keyDigits[Key.D5] = 5;
        _keyDigits[Key.D6] = 6;
        _keyDigits[Key.D7] = 7;
        _keyDigits[Key.D8] = 8;
        _keyDigits[Key.D9] = 9;

        _keyDigits[Key.NumPad0] = 0;
        _keyDigits[Key.NumPad1] = 1;
        _keyDigits[Key.NumPad2] = 2;
        _keyDigits[Key.NumPad3] = 3;
        _keyDigits[Key.NumPad4] = 4;
        _keyDigits[Key.NumPad5] = 5;
        _keyDigits[Key.NumPad6] = 6;
        _keyDigits[Key.NumPad7] = 7;
        _keyDigits[Key.NumPad8] = 8;
        _keyDigits[Key.NumPad9] = 9;

        _keyOps[Key.Add] = MathOperation.Add;
        _keyOps[Key.Subtract] = MathOperation.Subtract;
        _keyOps[Key.Multiply] = MathOperation.Multiply;
        _keyOps[Key.Divide] = MathOperation.Divide;

#if DEBUG
        _keyOps[Key.A] = MathOperation.Add;
        _keyOps[Key.S] = MathOperation.Subtract;
        _keyOps[Key.M] = MathOperation.Multiply;
        _keyOps[Key.D] = MathOperation.Divide;
#endif

    }

    public int CurrentValue
    {
        get
        {
            return (Int32)GetValue(CurrentValueProperty);
        }
        set
        {
            SetValue(CurrentValueProperty, value);
        }
    }

    public string CurrentInput
    {
        get
        {
            return GetValue(CurrentInputProperty) as string;
        }
        set
        {
            SetValue(CurrentInputProperty, value);
        }
    }

    public int PreviousValue
    {
        get
        {
            return (Int32)GetValue(PreviousValueProperty);
        }
        set
        {
            SetValue(PreviousValueProperty, value);
        }
    }

    public CalcWindow(int initialValue, MathOperation operation)
    {
        _initialValue = initialValue;
        _currentOp = operation;

        CurrentValue = _initialValue;
        PreviousValue = CurrentValue;

        InitializeComponent();

        InitializeButtons();
    }

    private void InitializeButtons()
    {
        _buttonDigits[_button0] = 0;
        _buttonDigits[_button1] = 1;
        _buttonDigits[_button2] = 2;
        _buttonDigits[_button3] = 3;
        _buttonDigits[_button4] = 4;
        _buttonDigits[_button5] = 5;
        _buttonDigits[_button6] = 6;
        _buttonDigits[_button7] = 7;
        _buttonDigits[_button8] = 8;
        _buttonDigits[_button9] = 9;

        _buttonOps[_buttonAdd] = MathOperation.Add;
        _buttonOps[_buttonSubtract] = MathOperation.Subtract;
        _buttonOps[_buttonMultiply] = MathOperation.Multiply;
        _buttonOps[_buttonDivide] = MathOperation.Divide;
    }
}

All that’s left is to the actual work of handling input. Since input can be from both keyboard and button clicks, let’s first define methods to handle the general operations, and we can hook up the input later.

Generic Input Handling

First, we’ll look at the generic input handlers. These are called by the mouse and keyboard handlers and they return true so they can notify the caller of whether they actually handled the operation.

The “C” button on the calculator will not only clear the current input, but set the current value to 0 (I’m not going to bother implementing a “CE” button that clears just the current input, but it would be easy to add).

private bool HandleClear()
{
   CurrentInput = "";
   CurrentValue = 0;

   return true;
}

When the user hits escape, you want to close the window, after setting the value back to the initial one.

private bool HandleEscape()
{
    _canceled = true;
    CurrentValue = _initialValue;

    Close();

    return true;
}

We need the _canceled flag because, as we’ll see below, when the window is deactivated, under normal circumstances we want to complete the current operation, but in the canceled case we want to do nothing.

When a user hits Enter or presses the Equals button, the current operation should be completed and the window closed. In this control, since we also want the current operation to be completed when the window is deactivated, hitting Equals will just cause the window to close and we’ll handle the calculation in the deactivation code.

private bool HandleEquals()
{
    //The final operation will be 
    //handled when the window closes
    Close();
    return true;
}

This calculator provides a back button to erase the previously-entered digit. If the input becomes empty, then the current value is set to 0.

private bool HandleBack()
{
    if (!string.IsNullOrEmpty(CurrentInput))
    {
        CurrentInput = CurrentInput.Substring(0, CurrentInput.Length - 1);
        //this is guaranteed to succeed because we validate when we add digits
        if (!string.IsNullOrEmpty(CurrentInput))
        {
            CurrentValue = Int32.Parse(CurrentInput);
        }
        else
        {
            CurrentValue = 0;
        }
    }
    return true;
}

When accepting new input, our button and keyboard handlers will ensure that the user can only enter digits, but we also need to make sure we handle integer overflow. An easy way to do this is to treat the new digit as a character and append it to the current input and then reparse it.

private bool HandleDigit(int digit)
{
    string newInput = CurrentInput + digit.ToString();
    int temp = 0;
    //make sure the full input is a number
    if (Int32.TryParse(newInput, out temp))
    {
        CurrentInput = newInput;
        CurrentValue = temp;
        return true;
    }
    return false;
}

Finally, when the user selects an operation we need to do one of two things. If there is no current input, then we can just change the current operation to the new one. This lets you change your mind, say if you hit ‘+’, but really wanted ‘-‘.

On the other hand, if the user has already entered a number, then you know you’re finishing up the current expression and the user wants to calculate the results and start a new expression. As an example,  consider the expression A + B. A is the initial value of the NumericEntryControl, the + is what triggered the calculator to show up, and B is the number the user entered after the calculator was visible. When the next operation is input, A + B should be resolved, placed into A, the current operation set, and the user can now enter the next B, and so on.

private bool HandleOperation(MathOperation op)
{
    if (CurrentInput.Length > 0)
    {
        ProcessCurrentOperation();
    }
    _currentOp = op;
    return true;
}

With that, we’ve seen all of the generic input handling. Now let’s move on to the actual math.

Doing the Math

The ProcessCurrentOperation method is the heart of this control as it updates all the values according to the user’s input. Here too we need to consider what happens if the user’s calculations cause an integer overflow. To detect this we need to use a combination of the checked keyword and some exception handling. To simplify things, if this happens, the whole calculation is canceled and the window closed. We also handle divide by zero errors by just setting the new value to 0.

(The checked keyword tells .NET to issue exceptions when integer math overflows. The default behavior depends on compilation and environment settings, but the unchecked behavior is usually what happens).

private void ProcessCurrentOperation()
{
    if (string.IsNullOrEmpty(CurrentInput))
    {
        return;
    }

    int newValue = 0;
    checked
    {
        try
        {
            switch (_currentOp)
            {
                case MathOperation.Add:
                    newValue = PreviousValue + CurrentValue;
                    break;
                case MathOperation.Subtract:
                    newValue = PreviousValue - CurrentValue;
                    break;
                case MathOperation.Multiply:
                    newValue = PreviousValue * CurrentValue;
                    break;
                case MathOperation.Divide:
                    if (CurrentValue != 0)
                    {
                        newValue = PreviousValue / CurrentValue;
                    }
                    else
                    {
                        newValue = CurrentValue;
                    }
                    break;
                default:
                    Debug.Assert(false, "Invalid operation. Should never happen!");
                    break;
            }

            CurrentInput = "";
            CurrentValue = newValue;
            PreviousValue = newValue;
        }
        catch (OverflowException)
        {
            //will cause the window to close, so we better cancel everything first
            _canceled = true;
            CurrentValue = _initialValue;

            MessageBox.Show("The result overflowed, calculation canceled.");
        }
    }
}

Deactivation

As we said before, deaction should cause the current operation to complete, unless the user has canceled.

protected override void OnDeactivated(EventArgs e)
{
    if (!_canceled)
    {
        ProcessCurrentOperation();
        if (IsVisible)
        {
            //window is still visible if we've 
            //clicked away from the window
            //before finishing calculation
            Close();
        }
    }
    base.OnDeactivated(e);
}

The only thing that remains is hooking up mouse and keyboard input to our existing methods.

Mouse Handling

These are already referenced in the XAML, and all they do is forward to the  generic handlers.

void opButton_Click(object sender, RoutedEventArgs e)
{
    MathOperation op = _buttonOps[sender as Button];
    e.Handled = HandleOperation(op);
}

void digitButton_Click(object sender, RoutedEventArgs e)
{
    int digit = _buttonDigits[sender as Button];
    e.Handled = HandleDigit(digit);
}

private void clearButton_click(object sender, RoutedEventArgs e)
{
    e.Handled = HandleClear();
}

private void backButton_Click(object sender, RoutedEventArgs e)
{
    e.Handled = HandleBack();
}

private void equalsButton_Click(object sender, RoutedEventArgs e)
{
    e.Handled = HandleEquals();
}

Keyboard Handling

To handle the keyboard, we need to preempt the normal message flow by looking at the preview messages (which flow from the top of the UI hierarchy down, rather than the normal messages which bubble from the bottom to the top). If we don’t do this, then when we hit keys like Enter, they go to the button with the current focus, when instead we want it to always cause the Equals functionality.

protected override void OnPreviewKeyDown(KeyEventArgs e)
{
    /*take over all keyboard input
     * this is important because if a button has focus 
     * and we hit enter then the button will be clicked
     * */

    int digit = 0;
    MathOperation op = MathOperation.None;
    if (_keyDigits.TryGetValue(e.Key, out digit))
    {
        e.Handled = HandleDigit(digit);
    }
    else if (_keyOps.TryGetValue(e.Key, out op))
    {
        e.Handled = HandleOperation(op);
    }
    else
    {
        switch (e.Key)
        {
            case Key.Enter:
                e.Handled = HandleEquals();
                break;
            case Key.Escape:
                e.Handled = HandleEscape();
                break;
            case Key.Back:
                e.Handled = HandleBack();
                break;
            case Key.C:
                e.Handled = HandleClear();
                break;
            default:
                e.Handled = false;
                break;
        }
    }
    base.OnPreviewKeyDown(e);
}

There you have it, a working pop-up calculator, usable from any other control you can subclass. As is often the case, there are a lot more features you could add to this basic implementation:calc_demo3

  • Undo
  • Floating-point support
  • Button highlights with corresponding key presses
  • More advanced display, showing current operator, previous values, etc.
  • More robust handling of overflow

Download Source Code

If you find this article useful, you will love my book, C# 4.0 How-To, which is filled with tips such as this.


Check out my latest book, the essential, in-depth guide to performance for all .NET developers:

Writing High-Performance.NET Code, 2nd Edition by Ben Watson. Available for pre-order:

Leave a Reply

Your email address will not be published. Required fields are marked *