Graphing Calculator Demo -- Part 1

It's been nearly a month since my last post but I haven't been idle. I've actually been working hard on a private project and, one day, I may document the steps I took to create it. For now, I'm going to go back to the MSDN samples. This time, I'm not going to bother with a one-to-one port of the program for two reasons: one, it's a very complicated program; two, I want to focus on the logical elements of the program rather than fiddle with the design aspects.

The topic here is centered on the Graphing Calculator Demo over at the .NET Framework Library. My goal is to understand the function plotting aspect of the demo. Once I've reached that goal then I may extend this series to include the parametric and 3d graphing but that may take a while since I will continue on with my private project before thinking about that.

First of all, you will notice that someone commented "This sample have [sic.] bugs when opened in Visual Studio 2008. The xaml design window cannot load the file because of errors." I couldn't comment back or edit that region in either Chrome or IE so I'll just write the solution here to kick things off.

Go to Window1.xaml and follow my fix below:

<!--JM: This doesn't work.
          <Setter Property="Background" Value="LinearGradient 0,0 0.03,.9 #ffcccccc #ffeeeeee"/>
    Check out below for fix.
-->
<Setter Property="Background">
    <Setter.Value>
        <LinearGradientBrush StartPoint="0,0" EndPoint="0.03,.9">
            <GradientStop Color="#ffcccccc" Offset="0" />
            <GradientStop Color="#ffeeeeee" Offset="1" />
        </LinearGradientBrush>
    </Setter.Value>
</Setter>

The most interesting part of this program, for me, is the parser so that's what I will talk about next time.

Concentric Rings Demo -- Part 2

Since the window is working nicely, I want to start drawing onto the canvas.

The final line of the constructor is CreateCircles(); which initializes some parameters for generating the circles then goes into a loop to instantiate and set-up each of the circles.

Every circle will be centered at (actually, near) the center of the canvas. It is very simple to find the center of the canvas:

double centerX = this.MainCanvas.ActualWidth / 2.0;
double centerY = this.MainCanvas.ActualHeight / 2.0;

We don't know the size of each person's screen so use the "ActualWidth/ActualHeight" property to get the rendered dimensions of the canvas width and height respectively. It is possible to set the height and width using the "Width/Height" property but it may not be rendered with that value for a variety of reasons (here about the height of the object):

Height is one of three writable properties on FrameworkElement that specify height information. The other two are MinHeight and MaxHeight. If there is a conflict between these values, the order of application for actual height determination is that first MinHeight must be honored, then MaxHeight, and finally, if it is within bounds, Height.

The next thing in the "CreateCircles" method is the list of valid colors for each circle:

Color[] colors = new Color[] { Colors.White, Colors.Green, Colors.Green, Colors.Lime };

Just a simple array of colors, this is good programming practice when you have a specific style in mind or otherwise already know which values should be valid as you then have a specific object with which you can check against to determine if a value is valid or which can be iterated through to retrieve valid values. In this case, we want to choose a color during the for-loop and with this approach we can just choose an arbitrary element from the colors list. To change the aesthetic, it's simply a matter of reorganizing the list rather than finding and changing all the hard-coded values.

The colors and center of the canvas are the only values needed to be determined before the loop. Actually, that's not quite true...many of the values are randomized. The random number object is instantiated in the constructor and the variable for it is declared as an instance variable beforehand:

public partial class Window1 : Window
{
    ...
    private Random rand;
    public Window1()
        {
            ...
            rand = new Random(this.GetHashCode());
            ...
        }
    ...
}

As noted in the comments of the GetHashCode documentation, it's not very useful on its own and, I might add, really not useful as a seed for a random number generator. In this case, the object is never different so the seed will always be the same so the random numbers will always have the same sequence. You can see this by running the program more than once--it's always the same. I would rather seed it with the time the program is run, that way the user will always get something exciting so I used "Environment.TickCount" instead of "this.GetHashCode()" for my rewrite.

When I was looking at the code, I noticed that while the animation looks like each circle is being created at a different time, the loop is instantiating them all at once. I saw each animation was set with a delay and concluded that that's where the magic happens. To find out if I was right, I commented out all the animation stuff in the for-loop to leave only:

Ellipse e = new Ellipse();
byte alpha = (byte)rand.Next(96,192);
int colorIndex = rand.Next(4);
e.Stroke = new SolidColorBrush(Color.FromArgb(alpha, colors[colorIndex].R, colors[colorIndex].G, colors[colorIndex].B));
e.StrokeThickness = rand.Next(1, 4);
e.Width = 0.0;
e.Height = 0.0;
double offsetX = 16 - rand.Next(32);
double offsetY = 16 - rand.Next(32);

this.MainCanvas.Children.Add(e);                

e.SetValue(Canvas.LeftProperty, centerX + offsetX);
e.SetValue(Canvas.TopProperty, centerY + offsetY);

Note: Elipses are shapes, they need using System.Windows.Shapes; at the top.

Of course, without the animation, the circles will not grow so I set "e.Width" and "e.Height" to "10.0" for testing purposes. Curiously, those 24 little circles were in the top-left corner of the screen rather than the center. I traced the problem to not inserting this.Show(); before calling the "CreateCircles" method. What that extra line does is force the window to render along with all its child elements (namely the maximized canvas) that way the layout manager will know where the center is.

All that's left to do is animate and since I learned about that (in more detail) from both the Alarm Clock Sample and Calculator Demo, I'm not going to cover it here. I will just note that these animation do not rely on a storyboard, a random delay (offsetXAnimation.BeginTime = TimeSpan.FromSeconds(delay);) is used to make it look like the circles are created at different times, and remember to put using System.Windows.Media.Animation; at the top.

Also, this works perfectly well without the "DispatcherTimer" and "lastTick" variables. I fail to understand their use here. Perhaps I will learn later.

Concentric Rings Demo -- Part 1

Finally something with more code and less markup...actually, a lot less markup. The only object in markup besides the main application window is a Canvas for drawing the rings. There will be no need to create a method akin to "InitializeComponent" this time and I will be able to really focus on C# and .NET without worrying about how to translate from XAML.

Here's the full translation of the XAML for the window:

public class Window1 : Window
{
    Canvas MainCanvas;

    public Window1()
    {
        //<window ...="" title="Concentric Rings" width="910" height="512">
        this.Title = "Concentric Rings";
        this.Width = 910;
        this.Height = 512;

        //<canvas name="MainCanvas" background="#FFE0E0E0"></canvas>
        MainCanvas = new Canvas();
        BrushConverter bc = new BrushConverter();
        SolidColorBrush brush = bc.ConvertFromString("#FFE0E0E0") as SolidColorBrush;
        MainCanvas.Background = brush;

        this.Content = MainCanvas;

    }
}

That's enough to get the window to show up with a gray canvas. The background was a little tricky. The "Background" property must be assigned a "Brush" type so I knew just setting it equal to the string wouldn't work. I noticed the "BrushConverter" class thanks to Visual Studio's autocomplete feature but I wanted to make sure I used it correctly so I searched the internet and found the perfect example. I also made a note to myself that "Canvas" comes from "System.Windows.Controls" and "BrushConverter" (as well as all other brush/color related stuff) comes from "System.Windows.Media".

All the rest of the code is already written so all I need to do is come to understand it. I want to finish the container before getting the circles to appear.

It is very useful to be able to control how the window looks to the user. Sometimes you want them to be able to resize, sometimes you want to remove the border. For the concentric rings, we want the window to be maximized, without any border or titlebar and without showing the taskbar.

These are the three important lines:

this.WindowState = WindowState.Maximized;
this.WindowStyle = WindowStyle.None;
this.ResizeMode = ResizeMode.NoResize;

I tried different combinations. Without any of those lines, the window looks like a normal application. By setting "WindowStyle.None", the titlebar (and buttons) disappear but it still has the border so it can be resized. If you keep the window style normal but set "ResizeMode.NoResize", you regain the title bar and still have a border but cannot change the size of the window. It is only with both "WindowStyle.None" and "ResizeMode.NoResize" does the boarder disappear. That's nice, but we want it to be maximized. Set "WindowState.Maximized" and you get a normal looking maximized window. Set "ResizeMode.NoResize" and magically, the taskbar disappears. What if we say it can be resized but get rid of the title bar? It still forces the taskbar away. So if we want it maximized, why go through the trouble of setting "NoResize" if it's not needed to get the look we want? Well, if you "alt-tab" to another program, you can see the taskbar, then you can right click on "Concentric Rings" and, if you set "NoResize", then you won't be able to restore the window, it will always be maximized. I personally think it would be better to have a windowed and fullscreen mode but perhaps that introduces unnecessary complications...I also think it's strange the programmer wants to force the window to stay maximized but set the "Width" and "Height" properties in XAML.

Since the window will be maximized at all times with no buttons or other GUI, it would be nice to offer a way to close the application without resorting to the "alt-tab" trick. For the last part of this part, I'll look at quitting with the "Escape" key.

It's very simple, especially if you've worked with any user interaction before. Just set the event handler for a keypress, check which key, and act appropriately:

public Window1()
{
    ...
    this.KeyDown += new System.Windows.Input.KeyEventHandler(Window1_KeyDown);
    ...
}

void Window1_KeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{
    if (e.Key == System.Windows.Input.Key.Escape)
            this.Close();
}

This is very similar to the Calculator.

Next time, I will try to understand the use of:

System.Windows.Threading.DispatcherTimer frameTimer;
private int lastTick;
private Random rand;

We've done animation without those objects before, so why use them now?

Calculator Demo -- Part 9

Getting the memory function to work was very similar to setting up the buttons so I didn't need to look up anything new. I had never seen menus for .NET yet so I did need to look up a couple things. Overall, however, it was extremely simple and didn't take much time at all.

As one would expect, rendering menus is very repetitive. I will only detail the "View" menu since that required a "checkable" menu item which makes it more interesting.

  • Rendering the menu
    • XAML:
      <menu dockpanel.dock="Top" height="26">
          ...
          <menuitem header="View">
               <menuitem name="StandardMenu" click="OnMenuStandard" ischeckable="true" ischecked="True" header="Standard"></menuitem>
          </menuitem>
          ...
      </menu>
      
    • C#:
      Menu menu = new Menu();
      DockPanel.SetDock(menu, Dock.Top);
      menu.Height = 26;
      ...
      MenuItem view = new MenuItem();
      view.Header = "View";
      StandardMenu = new MenuItem();
      StandardMenu.Name = "StandardMenu";
      StandardMenu.Click += new RoutedEventHandler(OnMenuStandard);
      StandardMenu.IsCheckable = true;
      StandardMenu.IsChecked = true;
      StandardMenu.Header = "Standard";
      view.Items.Add(StandardMenu);
      menu.Items.Add(view);
      ...
      MyPanel.Children.Add(menu);
      
    • Notes:

      The menu bar is set up first and is defined to be docked to the top of the window's DockPanel. The DockPanel was set up a long time ago to hold the grid. The grid was not docked so I guess DockPanel automatically places other children one after the other if they are not explicitly docked to a certain place. Here, the menu is docked to the top with DockPanel.SetDock(menu, Dock.Top);. I had a little trouble finding that but the .NET Framework Class Library pointed me in the right direction. My other problem was how to add children to the menus because there are a lot of collections and "add" methods to the "MenuItem" class. I was told "The submenu of the MenuItem is made up of the objects within the ItemCollection of a MenuItem." but I had to do some digging to discover "Items.Add(...)" was the way to go.

  • Making it do something
    • C#:
      void OnMenuStandard(object sender, RoutedEventArgs e)
      {
          //((MenuItem)ScientificMenu).IsChecked = false;
          ((MenuItem)StandardMenu).IsChecked = true; //for now always Standard
      }
      
    • Notes:

      I didn't have to change this at all. The original programmer was kind to leave a comment for how this is really supposed to work. There really should be two menu items to mirror windows calculator: one for the standard view and another for the scientific view. To keep things simple, there's no scientific view so there's no other menu item. For now, clicking this menu item keeps everything the same but it's clear how to modify it to add a scientific view, and how to manipulate other menu items.

I should mention that I declared "StandardMenu" outside all the methods so that it could be used by both the "InitializeThis" (for set-up) and "OnMenuStandard" (for action) methods.

Both versions now look and act exactly the same way (they both have the same bugs consequently). The Alarm Clock Sample port I wrote ended up having a smaller executable than the original but, for this calculator, the port is larger. That's interesting to me. I wonder where the difference lies.

Calculator Demo -- Part 8

The last thing I wanted to do was complete the calculator's memory. I saved it for last because I thought it would be very simple after finishing everything else. [Unfortunatley, I lied. The memory won't be the last thing to do. I forgot about the menu, but that should be even easier.] I noticed that the original code used a class called "PaperTrail" which is the last class defined in window1.xaml.cs.

  • PaperTrail
    • C#:
      private class PaperTrail
      {
          string args;
          
          public PaperTrail()
          {
          }
          public void AddArguments(string a)
          {
              args = a;
          }
          public void AddResult(string r)
          {
              PaperBox.Text += args + " = " + r + "\n";
          }
          public void Clear()
          {
              PaperBox.Text = string.Empty;
              args = string.Empty;
          }
      }
              
    • Notes:

      This is a very simple class. The constructor takes no arguments and does nothing. The only three methods are very simple in terms of how complex the code is but they don't do what I expected them to do. I thought something described as a "paper trail" would keep a record of the trail but it does no such thing. It calls upon "PaperBox" to keep the record.

  • PaperBox
    • C#:
              static MyTextBox PaperBox;
              ...
              PaperBox = new MyTextBox();
              Grid.SetRow(PaperBox, 1);
              Grid.SetColumn(PaperBox, 0);
              Grid.SetColumnSpan(PaperBox, 3);
              Grid.SetRowSpan(PaperBox, 5);
              PaperBox.IsReadOnly = true;
              PaperBox.VerticalScrollBarVisibility = ScrollBarVisibility.Visible;
              PaperBox.Margin = new Thickness(3.0,1.0,1.0,1.0);
              PaperBox.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
              
    • Notes:

      You can find "PaperBox" defined at the top of the "Window1" class definition. It is simply a textbox like "DisplayBox" but subclassed differently in the "Window1" constructor to have a different size using the "SetColumnSpan" and "SetRowSpan" methods, different interaction by setting the "IsReadOnly" property to "true", and a different visual style by making the scroll bars visible, and setting up the margins.

Now that there's a place to store memory information, there need to be appropriate buttons to interact with the memory. Microsoft's programmer decided to have Memory Clear, Memory Save, Memory Recall, and Memory Plus operations. The are found in the "ProcessOperation" method //Comments by original programmer:

  • Memory Clear
    • C#:
              case "BMemClear":
                  Memory = 0.0F;
                  DisplayMemory();
                  break;
              
    • Notes:

      The memory value is stored as a string "_mem_val" accessed through the property "Memory". Here we want to clear the memory, so the mathematical thing to do is set it to 0 (floating point of course). All of the operations which alter memory call the "DisplayMemory" method.

  • Memory Save
    • C#:
              case "BMemSave":
                  Memory = Convert.ToDouble(Display);
                  DisplayMemory();
                  EraseDisplay = true;
                  break;
              
    • Notes:

      Because of EraseDisplay = true;, if you save the display to memory when you are in the middle of an operation, you need to key-in another value (maybe the same one) if you want to perform a binary operation. That is, the number is saved to memory and continues to be displayed but any binary operation will not see it. Unary operations just take the value from the display so they will work as expected.

  • Memory Recall
    • C#:
              case "BMemRecall":
                  Display = /*val =*/ Memory.ToString();
                  UpdateDisplay();
                  //if (LastOper != Operation.None)   //using MR is like entring a digit
                      EraseDisplay = false;
                  break;
              
    • Notes:

      Because of "EraseDisplay = false;", memory recall is like entering a digit. I guess the commented-out partial conditional would have been to make MR act like completing a binary operation with the saved value.

  • Memory Plus
    • C#:
              case "BMemPlus":
                  d = Memory + Convert.ToDouble(Display);
                  Memory = d;
                  DisplayMemory();
                  EraseDisplay = true;
                  break;
              
    • Notes:

      The value in the display is added to the value in memory and then saved in memory. Like "BMemSave", this effectively erases the display so binary operations won't see it.

The DisplayMemory method is a bit naughty. It accesses "_mem_val" directly instead of through the "Memory" property. The author went through pains to start the variable name with an underscore so you would expect him to follow the practice of pretending he can't access it without the approperiate getter.... Anyway, it also uses a certain object called "BMemBox" which is defined in XAML so I had to port it to C#:

  • Memory Display Box
    • XAML:
              <textblock name="BMemBox" grid.column="3" grid.row="1" margin="10,17,10,17" grid.columnspan="2">Memory: [empty]</textblock>
              
    • C#:
              BMemBox = new TextBlock();
              BMemBox.Name = "BMemBox";
              Grid.SetColumn(BMemBox, 3);
              Grid.SetRow(BMemBox, 1);
              BMemBox.Margin = new Thickness(10.0, 17.0, 10.0, 17.0);
              Grid.SetColumnSpan(BMemBox, 2);
              BMemBox.Text = "Memory: [empty]";
              MyGrid.Children.Add(BMemBox);
              
    • Notes:

      This is very similar to rendering the buttons but there's no click event. The styles are all default too.

I thought this would be the last part to the Calculator Demo series but I forgot about the menu. The next part should be the last part then I will get to choose another program to analyse.

Calculator Demo -- Part 7

I discovered last time that every operation button invokes the "OperBtn_Click" event handler on click which calls the "ProcessOperation" method with the appropriate argument. I want to briefly touch upon each operation in the switch statement of both the "ProcessOperation" and "Calc" methods.

ProcessOperation //Comments by original programmer

  • Division
    • C#:
                      case "BDevide":
                          if (EraseDisplay)    //stil wait for a digit...
                          {  //stil wait for a digit...
                              LastOper = Operation.Devide;
                              break;
                          }
                          CalcResults();
                          LastOper = Operation.Devide;
                          LastValue = Display;
                          EraseDisplay = true;
                          break;
                      
    • Notes:

      Many of these are binary operations so if the display has not changed since the last operation that means a second number has not been entered. All of these binary operations wait for a second digit before trying to perform their respective operation or the results wouldn't make sense. Also of note is that these binary operations overwrite "LastOper" with themselves so that they will be performed when the next operation is clicked (this means the order of operations for this calculator is strictly left to right).

  • Multiplication
    • C#:
                      case "BMultiply":
                          if (EraseDisplay)    //stil wait for a digit...
                          {  //stil wait for a digit...
                              LastOper = Operation.Multiply;
                              break;
                          }
                          CalcResults();
                          LastOper = Operation.Multiply;
                          LastValue = Display;
                          EraseDisplay = true;
                          break;
                      
                      
    • Notes:

      Another binary operation like above so it follows the same scheme. If a division occurred before this case, "EraseDisplay" would be true so it would wait for the next digit. If a numerical key was pressed, "EraseDisplay" would be false so the last operation would be calculated (for example, division) and then "LastOper" would be set up so that the multiplication happens next (as long as a numerical key is pressed next).

  • Subtraction
    • C#:
                      case "BMinus":
                          if (EraseDisplay)    //stil wait for a digit...
                          {  //stil wait for a digit...
                              LastOper = Operation.Subtract;
                              break;
                          }
                          CalcResults();
                          LastOper = Operation.Subtract;
                          LastValue = Display;
                          EraseDisplay = true;
                          break;
                      
                      
    • Notes:

      If a numerical key is not pressed after the multiplication button--say, the subtraction button is pressed instead--then "LastOper" will be overridden by the subtraction. In this case, a multiplication would not be performed next, a subtraction would be in the wings.

  • Addition
    • C#:
                      case "BPlus":
                          if (EraseDisplay)
                          {  //stil wait for a digit...
                              LastOper = Operation.Add;
                              break;
                          }
                          CalcResults();
                          LastOper = Operation.Add;
                          LastValue = Display;
                          EraseDisplay = true;
                          break;
                      
                      
    • Notes:

      It's clear that the "ProcessOperation" method does not perform the actual operation. This method only sets up the operation for the calculation and asks for the calculation to be performed under the right conditions. One would think, it must be the "CalcResults" method which must be performing the operation. That's almost correct. As I investigated previously, "CalcResults" does one more check

  • Solution
    • C#:
                      case "BEqual":
                          if (EraseDisplay)    //stil wait for a digit...
                              break;
                          CalcResults();
                          EraseDisplay = true;
                          LastOper = Operation.None;
                          LastValue = Display;
                          //val = Display;
                          break;
                      
                      
    • Notes:

      This isn't a binary operation so why is it waiting for a second digit? I think they just don't want "=" to override any previous operation until there's actually something to calculate. That last comment is a little puzzling to me as well. I guess there was a variable "val" used to store the result but it's not needed since the display is already holding the result so you can always query the display to get the previous result.

  • Take the square root
    • C#:
                      case "BSqrt":
                          LastOper = Operation.Sqrt;
                          LastValue = Display;
                          CalcResults();
                          LastValue = Display;
                          EraseDisplay = true;
                          LastOper = Operation.None;
                          break;
                      
    • Notes:

      Finally, we get an example of a unary operation. It doesn't need to check if the display is empty, it just operates on whatever is in the display. The display defaults to "0" so there's no problem pushing the square root button immediately after starting the program. All the unary operations set "LastOper" and "LastValue" at the start. They set them again after the calculation if needed just like the binary operations.

  • Multiply by a percent
    • C#:
                      case "BPercent":
                          if (EraseDisplay)    //stil wait for a digit...
                          {  //stil wait for a digit...
                              LastOper = Operation.Percent;
                              break;
                          }
                          CalcResults();
                          LastOper = Operation.Percent;
                          LastValue = Display;
                          EraseDisplay = true;
                          //LastOper = Operation.None;
                          break;
                      
    • Notes:

      I wish percent acted as a unary operation, just dividing your number by 100 and displaying the result. But here, it is used as a binary operation for multiplying one number by another number's percentage value. You can't see that from the above except for the hint that given by the conditional that it is a binary operation. I will talk about the "Calc" method later to revisit how each operation is actually performed in code.

  • Take the reciprocal
    • C#:
                      case "BOneOver":
                          LastOper = Operation.OneX;
                          LastValue = Display;
                          CalcResults();
                          LastValue = Display;
                          EraseDisplay = true;
                          LastOper = Operation.None;
                          break;
                      
    • Notes:

      This is the final mathematical unary operation. It doesn't look much different from case "BSqrt" except for the names.

  • Clear all
    • C#:
                      case "BC":
                          LastOper = Operation.None;
                          Display = LastValue = string.Empty;
                          //Paper.Clear();
                          UpdateDisplay();
                          break;
                      
    • Notes:

      This is one of the operations which doesn't perform any calculations. It only operates on some variables to, well, clear them. "LastOper" is cleared with "Operation.None", "Display" and "LastValue" are cleared with "string.Empty", and "Paper", which is the history I haven't yet looked into, is cleared with its "Clear" method.

      A couple interesting things about C# is that there is a constant "string.Empty" supplied out of the box (which actually works differently than the empty string--""--as mentioned in Part 4) and that assignment is carried out right-to-left so Display = LastValue = string.Empty; makes sense and works as expected.
  • Clear Entry
    • C#:
                      case "BCE":
                          LastOper = Operation.None;
                          Display = LastValue;
                          UpdateDisplay();
                          break;
                      
    • Notes:

      If you notice you've made a mistake before you push "=", you can undo it by pressing "CE". This cancels whatever operation you've asked the calculator to perform and then sets the display to the last value showing before you keyed in another. Of course, this will only work with binary operations as the unary operations automatically carry out the calculation.

Calc [I will ignore any line that references "Paper"]
  • Division
    • C#:
                      case Operation.Devide:
                          Paper.AddArguments(LastValue + " / " + Display);
                          d = (Convert.ToDouble(LastValue) / Convert.ToDouble(Display));
                          CheckResult(d);
                          Paper.AddResult(d.ToString());
                          break;
                      
    • Notes:

      In order to do math, all values must be numerical types. Recall that "LastValue" and "Display" are strings so they have to be converted to a numerical type. In this case, they are converted to doubles which are 64bit floating point numbers. C# doesn't complain about infinities when dealing with floating point numbers and that makes sense given the IEEE specificiation. The "CheckResult" method is used to discover values that don't make sense (for this calculator).

  • Addition
    • C#:
                      case Operation.Add:
                          Paper.AddArguments(LastValue + " + " + Display);
                          d = Convert.ToDouble(LastValue) + Convert.ToDouble(Display);
                          CheckResult(d);
                          Paper.AddResult(d.ToString());
                          break;
                      
    • Notes:

      Recall that "LastValue" is the value shown in the display before the second operand is entered and "Display" is what is shown to the user. At this point, "Display" still holds the value of the second operand. The display will be updated after the final answer, stored in "d" is returned to the "CalcResults" method.

  • Multiplication
    • C#:
                      case Operation.Multiply:
                          Paper.AddArguments(LastValue + " * " + Display);
                          d = Convert.ToDouble(LastValue) * Convert.ToDouble(Display);
                          CheckResult(d);
                          Paper.AddResult(d.ToString());
                          break;
                      
    • Notes:

      For all of these operations, you just need to look at the assignment to "d" for how the operation is performed. Here, we want to multiply so "LastValue" and "Display" are converted from strings to doubles and then multiplied with the result stored in "d".

  • Multiply by a percent
    • C#:
                      case Operation.Percent:
                          //Note: this is different (but make more sense) then Windows calculator
                          Paper.AddArguments(LastValue + " % " + Display);
                          d = (Convert.ToDouble(LastValue) * Convert.ToDouble(Display)) / 100.0F;
                          CheckResult(d);
                          Paper.AddResult(d.ToString());
                          break;
                      
    • Notes:

      I don't think this makes more sense than Windows calculator. I would prefer the percent operation to be unary and simply divide the current display by 100 to get ready for whatever operation you want to perform next. When I read 50%, I think of 50/100 = 0.5 and end it. I don't think of 50 * 1 / 100. And I definitely don't think of a binary operation.

  • Subtraction
    • C#:
                      case Operation.Subtract:
                          Paper.AddArguments(LastValue + " - " + Display);
                          d = Convert.ToDouble(LastValue) - Convert.ToDouble(Display);
                          CheckResult(d);
                          Paper.AddResult(d.ToString());
                          break;
                      
    • Notes:

      The .NET Framework defines a lot of conversion methods within a variety of classes. I haven't looked at them all yet. And that doesn't even include all the different object castings you can perform.

  • Take the square root
    • C#:
                      case Operation.Sqrt:
                          Paper.AddArguments("Sqrt( " + LastValue + " )");
                          d = Math.Sqrt(Convert.ToDouble(LastValue));
                          CheckResult(d);
                          Paper.AddResult(d.ToString());
                          break;
                      
    • Notes:

      C# doesn't include a native Sqrt operation so it is implemented in the Math namespace.

  • Take the reciprocal
    • C#:
                      case Operation.OneX:
                          Paper.AddArguments("1 / " + LastValue);
                          d = 1.0F / Convert.ToDouble(LastValue);
                          CheckResult(d);
                          Paper.AddResult(d.ToString());
                          break;
                      
    • Notes:

      This programmer is very OCD about making sure all the numbers are represented as floating points. You don't need to but it's good practice to choose one way and stick to it. Here's a little something I made to test how division in particular is handled:

                      using System;
                      
                      namespace testconsole
                      {
                          class Program
                          {
                              static void Main(string[] args)
                              {
                                  Console.WriteLine(7 / 2);
                                  Console.WriteLine(7 / 2.0);
                                  Console.WriteLine(7 / 2F);
                                  Console.WriteLine(7 / 2.0F);
                                  
                                  Console.WriteLine(7.0 / 2);
                                  Console.WriteLine(7.0 / 2.0);
                                  Console.WriteLine(7.0 / 2F);
                                  Console.WriteLine(7.0 / 2.0F);
                                  
                                  Console.WriteLine(7F / 2);
                                  Console.WriteLine(7F / 2.0);
                                  Console.WriteLine(7F / 2F);
                                  Console.WriteLine(7F / 2.0F);
                                  
                                  Console.WriteLine(7.0F / 2);
                                  Console.WriteLine(7.0F / 2.0);
                                  Console.WriteLine(7.0F / 2F);
                                  Console.WriteLine(7.0F / 2.0F);
                                  
                                  Console.ReadLine();
                              }
                          }
                      }
                      
      Result:
      3
      3.5
      3.5
      3.5
      3.5
      3.5
      3.5
      3.5
      3.5
      3.5
      3.5
      3.5
      3.5
      3.5
      3.5
      3.5

      Of course, there are other ways to convert integers to floats but those are the most common shorthands.

CheckResults:

  • Discover invalid numbers
    • C#:
                      private void CheckResult(double d)
                      {
                          if (Double.IsNegativeInfinity(d) || Double.IsPositiveInfinity(d) || Double.IsNaN(d))
                              throw new Exception("Illegal value");
                      }
                      
    • Notes:

      There is very useful stuff to remember here. The facts that Double has a method to check for +/- infinity as well as values which are not numbers (NaN) and throwing exceptions. Very handy.

All that's left is displaying the memory and history (paper trail).

Calculator Demo -- Part 6

For the sake of organization and time, I have completed the animations as promised and I added all the other buttons to the grid. However, I only fully implemented the "+/-" operation because I thought it best to visit each operation in the next part.

  • Rendering the buttons
    • XAML:
      <Button Name="BPM" Click="OperBtn_Click" Background="Darkgray" Style="{StaticResource DigitBtn}"  Grid.Column="6" Grid.Row="5" >+/-</Button>
      
    • C#:
      Button BPM = new Button();
      BPM.Name = "BPM";
      BPM.Click += new RoutedEventHandler(OperBtn_Click);
      BPM.Background = Brushes.DarkGray;
      BPM.Style = DigitBtn; //try without resources
      Grid.SetColumn(BPM, 6);
      Grid.SetRow(BPM, 5);
      BPM.Content = "+/-";
      MyGrid.Children.Add(BPM);
      
    • Notes:

      This is exactly as the buttons I looked at previously except with a different event handler: BPM.Click += new RoutedEventHandler(OperBtn_Click);. Also, when doing some of the other operations there is a tooltip property set (eg. BMemPlus.ToolTip = "Add To Memory"; for the "M+" button).

I wanted the "+/-" button to work as expected so I followed the Click event:

  • OperBtn_Click
    • C#:
          private void OperBtn_Click(object sender, RoutedEventArgs e)
          {
              ProcessOperation(((Button)sender).Name.ToString());
          }
          
    • Notes:

      All the buttons have their name properties set. In the case of the "+/-" button we have BPM.Name = "BPM";. When the above handler is invoked it will then pass the name of the button as an argument to the ProcessOperation method.

  • ProcessOperation
    • C#:
          private void ProcessOperation(string s)
          {
              Double d = 0.0;
              switch (s)
              {
                  case "BPM":
                  LastOper = Operation.Negate;
                  LastValue = Display;
                  CalcResults();
                  LastValue = Display;
                  EraseDisplay = true;
                  LastOper = Operation.None;
                  break;
                  ...
              }
          }
          
    • Notes:

      Here we see what happens when calling ProcessOperation("BPM");. The variable "d" is not applicable to this case. First, the most recently clicked operation is saved into "LastOper" and the most recent value displayed is saved into "LastValue". By doing this before the calculation, the program is overriding whatever operation was clicked beforehand (that means, if you try to do "2*-2" by clicking "2,*,2,+/-" the multiplication will be forgotten). "LastValue" and "LastOper" deserve a closer look later. Next, the calculation is performed by calling "CalcResults". When the calculation is completed, the current display is again saved however the operation is set to null which is force other operations to not negate again. It is very important to do it this way because this calculator works with infix notation. When I first saw EraseDisplay = true; I thought it meant to actually erase the display but that doesn't make sense. What it is actually doing is taking a note so that the next operation will know whether it should erase the display (in the case of "ProcessKey") or whether to simply treat the display as empty (as in several cases of "ProcessOperation").

  • Operation LastOper
    • C#:
          private enum Operation
          {
              None,
              Devide,
              Multiply,
              Subtract,
              Add,
              Percent,
              Sqrt,
              OneX,
              Negate
          }
          private Operation LastOper;
          
    • Notes:

      I've used enums before for setting many properties in but the Alarm Clock Sample and this Calculator Demo but this is the first time I've really seen how they are useful. Here, they set us up so it will be very clear which operation is being used and we don't need to deal with strings. The compiler gives them numbers which is good for computers and we get a descriptive name which is good for us.

  • LastValue
    • C#:
          private string LastValue
          {
              get
              {
                  if (_last_val == string.Empty)
                  return "0";
                  return _last_val;
              }
              set
              {
                  _last_val = value;
              }
          }
          
    • Notes:

      This is a very simple property declaration but it shows us exactly why getters and setters are useful. This is a calculator application and it's not very mathematical to show an empty display area so it checks if the last value is an empty string (from "Clear All") and returns the string "0".

  • CalcResults
    • C#:
          private void CalcResults()
          {
              double d;
              if (LastOper == Operation.None)
                  return;
              
              d = Calc(LastOper);
              Display = d.ToString();
              
              UpdateDisplay();
          }
          
    • Notes:

      First, a variable, "d", is declared to save the numerical result of the upcoming calculation. If there is no operation to be performed (as is the case after "+/-" is completed), this method just returns and nothing happens. However, if there is an operation to be performed it calls the "Calc" method to do the dirty work. The result of the operation is sent to the display and the display is updated. Again, I am very aware of object oriented programming at work (I guess it's actually more of an example of procedural programming since all these functions/methods are in the same class).

  • Calc
    • C#:
          private double Calc(Operation LastOper)
          {
              double d = 0.0;
              
              try {
                  switch (LastOper)
                  {
                      ...
                      case Operation.Negate:
                      d = Convert.ToDouble(LastValue) * (-1.0F);
                      break;
                  }
              }
              catch {
                  d = 0;
                  Window parent = (Window)MyPanel.Parent;
                  //Paper.AddResult("Error");
                  MessageBox.Show(parent, "Operation cannot be perfomed", parent.Title);
              }
              
              return d;
          }
          
    • Notes:

      Again, I am only highlighting "+/-". The variable "d" is used store the result of the calculation and then we come to a Try/Catch block for exception handling. Many of the operations check the calculation to make sure there's nothing strange like infinities and then throw an exception which will be caught by the "catch" block. I will look at those next time. Notice the Operation.Negate case won't forcefully throw an exception since it's only multiplying the last value displayed by negative one and the last value should have already been checked for sanity. However, if something were to go wrong we can see that the error will be noted by adding it to "Paper" (not relevant now) and a standard message box will be rendered.

When you click on a button, a lot of things happen. I think it's really good that I chose to continue the Calculator Demo. In this part, I got to understand enums, try/except, and a bit more about properties and modular code. I will go through the other operations in the next part though I won't have to be nearly as detailed as here.

[update Jan 15, 2010]

I just noticed, I never actually talked about the animation though I did actually complete them at the time.

I will simply post my code with comments (these go in "InitializeThis"):

    ...
    //<Storyboard x:Key="playStoryboard">
    Storyboard playStoryboard = new Storyboard();
    this.RegisterName("playStoryboard", playStoryboard);
    //  <DoubleAnimation From="50"  To="40" Duration="0:0:0.25" RepeatBehavior="1x" AutoReverse="True" 
    //      Storyboard.TargetName="TB" Storyboard.TargetProperty="(Ellipse.Height)"/>
    DoubleAnimation TBHeightAnimation = new DoubleAnimation(50.0, 40.0, new Duration(new TimeSpan(0, 0, 0, 0, 250))); //guess
    TBHeightAnimation.RepeatBehavior = new RepeatBehavior(1.0); //1x gives the error "cannot implicitly convert int to RepeatBehaviour"
    TBHeightAnimation.AutoReverse = true;
    Storyboard.SetTargetName(TBHeightAnimation, "TB");
    Storyboard.SetTargetProperty(TBHeightAnimation, new PropertyPath(Ellipse.HeightProperty));
    playStoryboard.Children.Add(TBHeightAnimation);
    
    //  <DoubleAnimation From="50"  To="44" Duration="0:0:0.25" RepeatBehavior="1x" AutoReverse="True" 
    //      Storyboard.TargetName="TB" Storyboard.TargetProperty="(Ellipse.Width)"/>
    DoubleAnimation TBWidthAnimation = new DoubleAnimation(50.0, 44.0, new Duration(new TimeSpan(0, 0, 0, 0, 250)));
    TBWidthAnimation.RepeatBehavior = new RepeatBehavior(1.0);
    TBWidthAnimation.AutoReverse = true;
    Storyboard.SetTargetName(TBWidthAnimation, "TB");
    Storyboard.SetTargetProperty(TBWidthAnimation, new PropertyPath(Ellipse.WidthProperty));
    playStoryboard.Children.Add(TBWidthAnimation);
    //</Storyboard>
    MyGrid.Resources.Add("playStoryboard", playStoryboard);
    /* The animations make the ellipses become circles... (including Microsoft's) */
    ...
    

Near the definition of setter4

    this.RegisterName("TB", TB);
    ...
    //<ControlTemplate.Triggers>
    //    <Trigger Property="IsMouseOver" Value="true">
    Trigger IMO = new Trigger();
    IMO.Property = Button.IsMouseOverProperty; //guess
    IMO.Value = true;
    //        <Setter TargetName="TB" Property="Ellipse.Fill" Value="Lightblue" />
    Setter IMOSetter = new Setter(Ellipse.FillProperty, Brushes.LightBlue, "TB");
    IMO.Setters.Add(IMOSetter);
    //    </Trigger>
    ct.Triggers.Add(IMO);
    
    //    <Trigger Property="IsPressed" Value="true">
    Trigger IP = new Trigger();
    IP.Property = Button.IsPressedProperty;
    IP.Value = true;
    //        <Setter TargetName="TB" Property="Ellipse.Fill" Value="Blue" />
    Setter IPSetter = new Setter(Ellipse.FillProperty, Brushes.Blue, "TB");
    IP.Setters.Add(IPSetter);
    //    </Trigger>
    ct.Triggers.Add(IP);
    
    //    <EventTrigger RoutedEvent="ButtonBase.Click">
    //http://msdn.microsoft.com/en-us/library/system.windows.controls.primitives.buttonbase.aspx
    EventTrigger BBC = new EventTrigger(System.Windows.Controls.Primitives.ButtonBase.ClickEvent);
    //        <EventTrigger.Actions>
    //            <BeginStoryboard Name="playStoryboard" Storyboard="{StaticResource playStoryboard}"/>
    BeginStoryboard BsPs = new BeginStoryboard();
    BsPs.Storyboard = (Storyboard)MyGrid.Resources["playStoryboard"];
    BsPs.Name = "playStoryboard";
    //        </EventTrigger.Actions>
    BBC.Actions.Add(BsPs);
    //    </EventTrigger>
    ct.Triggers.Add(BBC);
    //</ControlTemplate.Triggers>
    

[/update]