Today, I'll walk you through creating one. Note this tutorial is technically for Silverlight, but except for the marked sections, it can be applied wholesale to WinForms or WPF. Most if this is simple stack logic, and that exists on all .NET platforms.
Note: I'm assuming you are familiar with stack operations. If not, please visit the linked MSDN page for information.
Before we start programming, you have to understand postfix notation. Consider the following:
Expand|Select|Wrap|Line Numbers
- 5 6 +
- Push(5)
- Push(6)
- var b = Pop()
- var a = Pop()
- Push(a + b)
- Peek()
Consider a more complicated expression:
Expand|Select|Wrap|Line Numbers
- 10 4 6 + 9 * -
- Push(10)
- Push(4)
- Push(6)
- var b = Pop()
- var a = Pop()
- Push(a + b)
- Push(9)
- var b = Pop()
- var a = Pop()
- Push(a * b)
- var b = Pop()
- var a = Pop()
- Push(a - b)
- Peek()
The basic logic is to push numbers onto the stack until you encounter an operator. At that point, you pop the top two off the stack, evaluate them with the operator, and push the result back onto the stack. After evaluation, Peek for display.
In RPN calculator terms, there is an ENTER button to push a value onto the stack. Also, to save time, if you're currently entering a number, pushing an operator button will also push the number onto the stack.
So for
5 6 + 3 *
, we'd push:5 [ENTER] 6 [+] 3[*].
For
1 6 2 / 3 4 / * +
(which is the equivalent of 1 + (( 6 / 2 ) * ( 3 / 4 ))
, you'd push:1 [ENTER] 6 [ENTER] 2 [/] 3 [ENTER] 4 [/][*] [+]
On a standard calculator, you'd have to use the Memory function to do this with the proper order of operation. With a graphing calculator, you'd have to use parenthesis. But since you can keep state in the stack, you don't have to worry about any of that with an RPN calc.
So, without further ado, lets get into the code.
Setup
We'll obviously need a stack:
Expand|Select|Wrap|Line Numbers
- private Stack<double> stack;
Expand|Select|Wrap|Line Numbers
- private Dictionary<Key, string> opKeys = new Dictionary<Key, string>();
- private Dictionary<Key, string> numKeys = new Dictionary<Key, string>();
- private void InitializeDictionaries()
- {
- opKeys.Add(Key.C, "C");
- opKeys.Add(Key.Back, "B");
- opKeys.Add(Key.Add, "+");
- opKeys.Add(Key.Subtract, "-");
- opKeys.Add(Key.Multiply, "*");
- opKeys.Add(Key.Divide, "/");
- opKeys.Add(Key.Enter, "E");
- opKeys.Add(Key.Decimal, ".");
- numKeys.Add(Key.D0, "0");
- numKeys.Add(Key.D1, "1");
- numKeys.Add(Key.D2, "2");
- numKeys.Add(Key.D3, "3");
- numKeys.Add(Key.D4, "4");
- numKeys.Add(Key.D5, "5");
- numKeys.Add(Key.D6, "6");
- numKeys.Add(Key.D7, "7");
- numKeys.Add(Key.D8, "8");
- numKeys.Add(Key.D9, "9");
- numKeys.Add(Key.NumPad0, "0");
- numKeys.Add(Key.NumPad1, "1");
- numKeys.Add(Key.NumPad2, "2");
- numKeys.Add(Key.NumPad3, "3");
- numKeys.Add(Key.NumPad4, "4");
- numKeys.Add(Key.NumPad5, "5");
- numKeys.Add(Key.NumPad6, "6");
- numKeys.Add(Key.NumPad7, "7");
- numKeys.Add(Key.NumPad8, "8");
- numKeys.Add(Key.NumPad9, "9");
- }
Expand|Select|Wrap|Line Numbers
- private Dictionary<string, Func<double, double, double>> op =
- new Dictionary<string, Func<double, double, double>>();
- private void InitializeOp()
- {
- op.Add("+", (a, B) => a + B);
- op.Add("-", (a, B) => a - B);
- op.Add("*", (a, B) => a * B);
- op.Add("/", (a, B) => a / B);
- }
This doesn't go in the project. This is just an example of using Funcs in a dictionary.
Expand|Select|Wrap|Line Numbers
- double a = 5, b = 6;
- double result = op["+"](5, 6);
Also, two more bools we'll need to keep track of state:
Expand|Select|Wrap|Line Numbers
- private bool clearOnNext, errorState;
Silverlight Specific: XAML MARKUP (display code)
Here's the XAML markup we're using. This could be replicated by WinForms, but I prefer to work with Silverlight/WPF whenever I can.
Expand|Select|Wrap|Line Numbers
- <Grid x:Name="LayoutRoot" Background="White">
- <Grid>
- <Grid.RowDefinitions>
- <RowDefinition Height="Auto" />
- <RowDefinition Height="*" />
- </Grid.RowDefinitions>
- <!-- Max length: 15 -->
- <Grid Grid.Row="0">
- <Grid.ColumnDefinitions>
- <ColumnDefinition Width="30" />
- <ColumnDefinition Width="50" />
- <ColumnDefinition Width="*"/>
- </Grid.ColumnDefinitions>
- <Border BorderBrush="Black" BorderThickness="1" CornerRadius="3">
- <Grid>
- <Grid.RowDefinitions>
- <RowDefinition />
- <RowDefinition />
- </Grid.RowDefinitions>
- <TextBlock Text="Stack Depth" Grid.Row="0" FontSize="8" TextWrapping="Wrap" VerticalAlignment="Bottom" HorizontalAlignment="Center" />
- <TextBlock Text="0" Grid.Row="1" VerticalAlignment="Bottom" HorizontalAlignment="Center" x:Name="stackDepthTextBlock" />
- </Grid>
- </Border>
- <Border Grid.Column="1" BorderBrush="Black" BorderThickness="1" CornerRadius="3">
- <Grid>
- <Grid.RowDefinitions>
- <RowDefinition />
- <RowDefinition />
- </Grid.RowDefinitions>
- <Button x:Name="copyButton" Content="Copy" Click="copyButton_Click" />
- <Button x:Name="pasteButton" Content="Paste" Grid.Row="1" Click="pasteButton_Click" />
- </Grid>
- </Border>
- <TextBox IsReadOnly="True" x:Name="displayTextBox" Text=""
- Grid.Column="2" Grid.Row="0" Grid.RowSpan="2" HorizontalAlignment="Stretch" TextAlignment="Right"
- FontFamily="Courier New" FontSize="26" FontWeight="ExtraBold"
- KeyUp="DisplayTextBox_KeyUp" />
- </Grid>
- <Grid Grid.Row="1">
- <Grid.RowDefinitions>
- <RowDefinition />
- <RowDefinition />
- <RowDefinition />
- <RowDefinition />
- <RowDefinition />
- </Grid.RowDefinitions>
- <Grid.ColumnDefinitions>
- <ColumnDefinition />
- <ColumnDefinition />
- <ColumnDefinition />
- <ColumnDefinition />
- </Grid.ColumnDefinitions>
- <!-- Buttons -->
- <Button Content="Enter" Tag="E" Style="{StaticResource ViewboxButton}" Margin="2" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Click="Button_Click" IsTabStop="False" />
- <Button Content="C" Tag="C" Style="{StaticResource ViewboxButton}" Margin="2" Grid.Row="0" Grid.Column="2" Grid.ColumnSpan="1" Click="Button_Click" IsTabStop="False" />
- <Button Content="?" Tag="B" Style="{StaticResource ViewboxButton}" Margin="2" Grid.Row="0" Grid.Column="3" Grid.ColumnSpan="1" Click="Button_Click" IsTabStop="False" />
- <Button Content="7" Tag="7" Style="{StaticResource ViewboxButton}" Margin="2" Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="1" Click="Button_Click" IsTabStop="False" />
- <Button Content="8" Tag="8" Style="{StaticResource ViewboxButton}" Margin="2" Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="1" Click="Button_Click" IsTabStop="False" />
- <Button Content="9" Tag="9" Style="{StaticResource ViewboxButton}" Margin="2" Grid.Row="1" Grid.Column="2" Grid.ColumnSpan="1" Click="Button_Click" IsTabStop="False" />
- <Button Content="÷" Tag="/" Style="{StaticResource ViewboxButton}" Margin="2" Grid.Row="1" Grid.Column="3" Grid.ColumnSpan="1" Click="Button_Click" IsTabStop="False" />
- <Button Content="4" Tag="7" Style="{StaticResource ViewboxButton}" Margin="2" Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="1" Click="Button_Click" />
- <Button Content="5" Tag="8" Style="{StaticResource ViewboxButton}" Margin="2" Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="1" Click="Button_Click" />
- <Button Content="6" Tag="9" Style="{StaticResource ViewboxButton}" Margin="2" Grid.Row="2" Grid.Column="2" Grid.ColumnSpan="1" Click="Button_Click" />
- <Button Content="×" Tag="*" Style="{StaticResource ViewboxButton}" Margin="2" Grid.Row="2" Grid.Column="3" Grid.ColumnSpan="1" Click="Button_Click" />
- <Button Content="4" Tag="4" Style="{StaticResource ViewboxButton}" Margin="2" Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="1" Click="Button_Click" IsTabStop="False" />
- <Button Content="5" Tag="5" Style="{StaticResource ViewboxButton}" Margin="2" Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="1" Click="Button_Click" IsTabStop="False" />
- <Button Content="6" Tag="6" Style="{StaticResource ViewboxButton}" Margin="2" Grid.Row="2" Grid.Column="2" Grid.ColumnSpan="1" Click="Button_Click" IsTabStop="False" />
- <Button Content="×" Tag="*" Style="{StaticResource ViewboxButton}" Margin="2" Grid.Row="2" Grid.Column="3" Grid.ColumnSpan="1" Click="Button_Click" IsTabStop="False" />
- <Button Content="1" Tag="1" Style="{StaticResource ViewboxButton}" Margin="2" Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="1" Click="Button_Click" IsTabStop="False" />
- <Button Content="2" Tag="2" Style="{StaticResource ViewboxButton}" Margin="2" Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="1" Click="Button_Click" IsTabStop="False" />
- <Button Content="3" Tag="3" Style="{StaticResource ViewboxButton}" Margin="2" Grid.Row="3" Grid.Column="2" Grid.ColumnSpan="1" Click="Button_Click" IsTabStop="False" />
- <Button Content="-" Tag="-" Style="{StaticResource ViewboxButton}" Margin="2" Grid.Row="3" Grid.Column="3" Grid.ColumnSpan="1" Click="Button_Click" IsTabStop="False" />
- <Button Content="0" Tag="0" Style="{StaticResource ViewboxButton}" Margin="2" Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="2" Click="Button_Click" IsTabStop="False" />
- <Button Content="." Tag="." Style="{StaticResource ViewboxButton}" Margin="2" Grid.Row="4" Grid.Column="2" Grid.ColumnSpan="1" Click="Button_Click" IsTabStop="False" />
- <Button Content="+" Tag="+" Style="{StaticResource ViewboxButton}" Margin="2" Grid.Row="4" Grid.Column="3" Grid.ColumnSpan="1" Click="Button_Click" IsTabStop="False" />
- <!-- /Buttons -->
- </Grid>
- </Grid>
- </Grid>
Expand|Select|Wrap|Line Numbers
- <Style x:Key="ViewboxButton" TargetType="Button">
- <Setter Property="Background" Value="#FF1F3B53"/>
- <Setter Property="Foreground" Value="#FF000000"/>
- <Setter Property="Padding" Value="3"/>
- <Setter Property="BorderThickness" Value="1"/>
- <Setter Property="BorderBrush">
- <Setter.Value>
- <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
- <GradientStop Color="#FFA3AEB9" Offset="0"/>
- <GradientStop Color="#FF8399A9" Offset="0.375"/>
- <GradientStop Color="#FF718597" Offset="0.375"/>
- <GradientStop Color="#FF617584" Offset="1"/>
- </LinearGradientBrush>
- </Setter.Value>
- </Setter>
- <Setter Property="Template">
- <Setter.Value>
- <ControlTemplate TargetType="Button">
- <Grid>
- <vsm:VisualStateManager.VisualStateGroups>
- <vsm:VisualStateGroup x:Name="CommonStates">
- <vsm:VisualState x:Name="Normal"/>
- <vsm:VisualState x:Name="MouseOver">
- <Storyboard>
- <DoubleAnimation Duration="0" Storyboard.TargetName="BackgroundAnimation" Storyboard.TargetProperty="Opacity" To="1"/>
- <ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)" To="#F2FFFFFF"/>
- <ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[2].(GradientStop.Color)" To="#CCFFFFFF"/>
- <ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[3].(GradientStop.Color)" To="#7FFFFFFF"/>
- </Storyboard>
- </vsm:VisualState>
- <vsm:VisualState x:Name="Pressed">
- <Storyboard>
- <ColorAnimation Duration="0" Storyboard.TargetName="Background" Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)" To="#FF6DBDD1"/>
- <DoubleAnimation Duration="0" Storyboard.TargetName="BackgroundAnimation" Storyboard.TargetProperty="Opacity" To="1"/>
- <ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[0].(GradientStop.Color)" To="#D8FFFFFF"/>
- <ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)" To="#C6FFFFFF"/>
- <ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[2].(GradientStop.Color)" To="#8CFFFFFF"/>
- <ColorAnimation Duration="0" Storyboard.TargetName="BackgroundGradient" Storyboard.TargetProperty="(Rectangle.Fill).(GradientBrush.GradientStops)[3].(GradientStop.Color)" To="#3FFFFFFF"/>
- </Storyboard>
- </vsm:VisualState>
- <vsm:VisualState x:Name="Disabled">
- <Storyboard>
- <DoubleAnimation Duration="0" Storyboard.TargetName="DisabledVisualElement" Storyboard.TargetProperty="Opacity" To=".55"/>
- </Storyboard>
- </vsm:VisualState>
- </vsm:VisualStateGroup>
- <vsm:VisualStateGroup x:Name="FocusStates">
- <vsm:VisualState x:Name="Focused">
- <Storyboard>
- <DoubleAnimation Duration="0" Storyboard.TargetName="FocusVisualElement" Storyboard.TargetProperty="Opacity" To="1"/>
- </Storyboard>
- </vsm:VisualState>
- <vsm:VisualState x:Name="Unfocused" />
- </vsm:VisualStateGroup>
- </vsm:VisualStateManager.VisualStateGroups>
- <Border x:Name="Background" CornerRadius="3" Background="White" BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}">
- <Grid Background="{TemplateBinding Background}" Margin="1">
- <Border Opacity="0" x:Name="BackgroundAnimation" Background="#FF448DCA" />
- <Rectangle x:Name="BackgroundGradient" >
- <Rectangle.Fill>
- <LinearGradientBrush StartPoint=".7,0" EndPoint=".7,1">
- <GradientStop Color="#FFFFFFFF" Offset="0" />
- <GradientStop Color="#F9FFFFFF" Offset="0.375" />
- <GradientStop Color="#E5FFFFFF" Offset="0.625" />
- <GradientStop Color="#C6FFFFFF" Offset="1" />
- </LinearGradientBrush>
- </Rectangle.Fill>
- </Rectangle>
- </Grid>
- </Border>
- <Viewbox>
- <ContentPresenter Content="{TemplateBinding Content}" />
- </Viewbox>
- <Rectangle x:Name="DisabledVisualElement" RadiusX="3" RadiusY="3" Fill="#FFFFFFFF" Opacity="0" IsHitTestVisible="false" />
- <Rectangle x:Name="FocusVisualElement" RadiusX="2" RadiusY="2" Margin="1" Stroke="#FF6DBDD1" StrokeThickness="1" Opacity="0" IsHitTestVisible="false" />
- </Grid>
- </ControlTemplate>
- </Setter.Value>
- </Setter>
- </Style>
If you're not doing this in Silverlight, the important thing to note for the rest of this tutorial is that I'm assuming you created a Read Only TextBox named "displayTextBox". You can name it whatever you like, just make sure to change it in the code as well. Another thing I've assumed is that you've created all your buttons, and set their Tag properties to their numeral or operator value. Also, they all should call the same handler for their Click event: "Button_Click". One more thing, I've attached an event handler to the KeyUp event of the displayTextBox.
Here's a property we'll be using for convenience sake:
Expand|Select|Wrap|Line Numbers
- private double current
- {
- get
- {
- if (displayTextBox.Text == string.Empty)
- displayTextBox.Text = "0";
- return double.Parse(displayTextBox.Text);
- }
- }
Initialization
Here's how our constructor will look:
Expand|Select|Wrap|Line Numbers
- public MainPage()
- {
- InitializeComponent();
- InitializeDictionaries();
- InitializeOp();
- stack = new Stack<double>();
- this.Loaded += (s, ea) =>
- {
- if (!App.Current.IsRunningOutOfBrowser)
- System.Windows.Browser.HtmlPage.Plugin.Focus();
- displayTextBox.Focus();
- };
- clearOnNext = false;
- errorState = false;
- }
Methods
The simplest method we need to add has to handle input. We'll call this one from handlers.
Expand|Select|Wrap|Line Numbers
- private void ProcessInput(string input)
- {
- if (clearOnNext)
- {
- displayTextBox.Text = string.Empty;
- clearOnNext = false;
- errorState = false;
- }
- displayTextBox.Text += input;
- }
We'll also need a method to process special characters like "Clear", "Enter", "Backspace", and "Decimal" as well as the operators
Expand|Select|Wrap|Line Numbers
- private void ProcessSpecial(string input)
- {
- switch (input)
- {
- case "C":
- if (displayTextBox.Text.Length > 0)
- displayTextBox.Text = string.Empty;
- else
- {
- stack.Clear();
- RefreshDepthText();
- }
- clearOnNext = false;
- break;
- case "B":
- if (!clearOnNext && displayTextBox.Text.Length > 0)
- displayTextBox.Text = displayTextBox.Text.Substring(0, displayTextBox.Text.Length - 1);
- break;
- case ".":
- if (!displayTextBox.Text.Contains("."))
- {
- if (displayTextBox.Text.Length < 1 || errorState)
- ProcessInput("0.");
- else
- ProcessInput(".");
- }
- break;
- case "E":
- Enter();
- break;
- case "+":
- case "-":
- case "*":
- case "/":
- DoOp(input);
- break;
- }
- }
Case "C" is clear. First click, it clears the display. Second (or first if the display is already clear) clears the stack.
Case "B" is backspace. Substring if there's room to.
Case "." handles the decimal. We only allow one of those, and for visual's sake, add a 0 in front if it's the first button clicked.
Case "E" is enter. We'll write that method soon.
The remaining cases are operators. We'll also look at that method soon.
Calculator Logic methods
Here's the Enter method now:
Expand|Select|Wrap|Line Numbers
- private void Enter()
- {
- if (!errorState)
- {
- stack.Push(current);
- RefreshDepthText();
- clearOnNext = true;
- }
- }
Here's the method where we actually do an operation:
Expand|Select|Wrap|Line Numbers
- private void DoOp(string input)
- {
- if (!clearOnNext)
- {
- stack.Push(current);
- }
- if (stack.Count < 2)
- {
- errorState = true;
- clearOnNext = true;
- displayTextBox.Text = "OUT OF STACK";
- return;
- }
- double b = stack.Pop();
- double a = stack.Pop();
- stack.Push(op[input](a, B));
- double res = stack.Peek();
- if (res == double.NegativeInfinity || res == double.PositiveInfinity || res == double.NaN)
- {
- stack.Clear();
- RefreshDepthText();
- errorState = true;
- clearOnNext = true;
- displayTextBox.Text = "DIV BY ZERO";
- return;
- }
- displayTextBox.Text = stack.Peek().ToString();
- RefreshDepthText();
- clearOnNext = true;
- }
Anyway, if everything was successful, Peek the value into the textbox, and move on.
Now, a keen observer would note that while we're set up to handle the calculator logic now, we have no way of actually entering numbers or operators. Have no fear, that's the next section.
Input logic
We've set up event handlers for the KeyUp on the readonly textbox as well as handling the Click event for each button. Here's the code for those event handlers:
Expand|Select|Wrap|Line Numbers
- private void Button_Click(object sender, RoutedEventArgs e)
- {
- string value = (sender as Button).Tag as string;
- if (numKeys.ContainsValue(value))
- ProcessInput(value);
- else
- ProcessSpecial(value);
- displayTextBox.Focus();
- }
- private void DisplayTextBox_KeyUp(object sender, KeyEventArgs e)
- {
- if (opKeys.ContainsKey(e.Key))
- ProcessSpecial(opKeys[e.Key]);
- else if (numKeys.ContainsKey(e.Key))
- ProcessInput(numKeys[e.Key].ToString());
- else return;
- }
These don't hold much importance, just necessary for little things:
Expand|Select|Wrap|Line Numbers
- private void RefreshDepthText()
- {
- stackDepthTextBlock.Text = stack.Count.ToString();
- }
- private void copyButton_Click(object sender, RoutedEventArgs e)
- {
- Clipboard.SetText(current.ToString());
- }
- private void pasteButton_Click(object sender, RoutedEventArgs e)
- {
- if (!Clipboard.ContainsText())
- return;
- string s = Clipboard.GetText();
- double x;
- if (double.TryParse(s, out x))
- displayTextBox.Text = s;
- }
Cross posted from my blog.