Calculator Demo — Part 5

It took so much effort to style the buttons that I'm not going to do the animations until next time. Instead of playing around with the animations like I said I would, I just made sure pressing the buttons would put the respective value into the display area.

I had to do a lot of research to translate the XAML for this part into C# so I will follow the same structure that I tried in Alarm Clock Sample — Part 6 (this time using the sourcecode shortcode Syntax Highlighter).

  • Defining the button style
    • XAML:
      <Style x:Key="DigitBtn"  TargetType="{x:Type Button}">
      
    • C#:
               Style DigitBtn = new Style();
               DigitBtn.TargetType = typeof(Button);
    • Notes:
      I simply looked up the x:Type Markup Extension and followed my nose after reading "x:Type is essentially a markup extension equivalent for a typeof() operator in C#". Basically, this is telling the complier that the program will only be applying this style to Button objects.
  • Setting the "Focusable" property
    • XAML:
      <Setter Property="Focusable" Value="False"/>
    • C#:
      Setter setter1 = new Setter(Button.FocusableProperty, false);
      DigitBtn.Setters.Add(setter1);
    • Notes:
      Since this is the first "Setter" I've seen, I needed to look up the constructor. However, I still didn't understand what all this was for until I read WPF Data Binding - DataTriggers though I only cared about the Data Binding part. It seems that because we are targeting buttons, we can set any properties that buttons have. This really makes me think of CSS classes because this program is going to have a bunch of buttons all with the same style so it makes sense to set up the style first and then, when you create the button element, you specify which style to use (similar to <input type="button" value="Submit" class="DigitBtn"> for HTML+CSS).
  • Setting the "FontSize" property
    • XAML:
      <Setter Property="FontSize" Value="14pt"/>
    • C#:
      LengthConverter lc = new LengthConverter();
      string qualifiedDouble = "14pt";
      Setter setter2 = new Setter(Button.FontSizeProperty, lc.ConvertFrom(qualifiedDouble));
      DigitBtn.Setters.Add(setter2);
    • Notes:
      Wow, so much work for something so simple. I would have never guessed this on my own...I know, I tried for quite a while. Thankfully, someone from the internets had the answer (and a pretty good explanation).
  • Setting the "Margin" property
    • XAML:
      <Setter Property="Margin" Value="0"/>
    • C#:
      Setter setter3 = new Setter(Button.MarginProperty, new Thickness(0));
      DigitBtn.Setters.Add(setter3);
    • Notes:
      Who would have guessed margins take values of Thickness Structures? It makes sense now that I look closer. Instead of specifying that something has a left, top, bottom, and right margin you just say it has a margin and you define a margin to be something that has left, top, bottom, and right properties.

The final property "Template" is much more involved so I will describe what I did from the inside-out. Of course, I didn't deal with any animations or other triggers. I just wanted the buttons to show up and work.

  • Presenting the button's content
    • XAML:
      <ContentPresenter Content="{TemplateBinding Content}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
    • C#:
      FrameworkElementFactory CP = new FrameworkElementFactory(typeof(ContentPresenter));
      Binding TBinding = new Binding("Content"){RelativeSource = RelativeSource.TemplatedParent, Mode = BindingMode.OneWay};
      CP.SetValue(ContentPresenter.ContentProperty, TBinding);
      CP.SetValue(HorizontalAlignmentProperty, HorizontalAlignment.Center);
      CP.SetValue(VerticalAlignmentProperty, VerticalAlignment.Center);
    • Notes:
      All of this gets nested inside a ControlTemplate which was giving me a lot of problems because it doesn't work the same way as other containers (in C#--they all look the same in XAML). Fortunately, a quick Google search came up with Changing the drawing style of a button from code which introduced me to FrameworkElementFactory. I had to read two more sources of information to figure out Content="{TemplateBinding Content}": one, TemplateBinding Markup Extension, told me that "A TemplateBinding is an optimized form of a Binding for template scenarios, analogous to a Binding constructed with {Binding RelativeSource={RelativeSource TemplatedParent} Mode=OneWay}."; two, How to: Create a Binding in Code, had an extremely useful comment by LukeSkywalker (of all people). I tried running the code without the ContentPresenter and, lo and behold, the button's content, which holds the text, didn't show.
  • Styling the Button
    • XAML:
      <Ellipse Width="57" Height="49" x:Name="TB"  StrokeThickness="1" Stroke="{TemplateBinding Foreground}" Fill="{TemplateBinding Background}" HorizontalAlignment="Center" VerticalAlignment="Center" />
    • C#:
      FrameworkElementFactory TB = new FrameworkElementFactory(typeof(Ellipse), "TB");
      TB.SetValue(Ellipse.WidthProperty, 57.0);
      TB.SetValue(Ellipse.HeightProperty, 49.0);
      TB.SetValue(Ellipse.StrokeThicknessProperty, 1.0);
      TB.SetValue(Ellipse.StrokeProperty, new Binding("Foreground") { RelativeSource = RelativeSource.TemplatedParent, Mode = BindingMode.OneWay });
      TB.SetValue(Ellipse.FillProperty, new Binding("Background") { RelativeSource = RelativeSource.TemplatedParent, Mode = BindingMode.OneWay });
      TB.SetValue(Ellipse.HorizontalAlignmentProperty, HorizontalAlignment.Center);
      TB.SetValue(Ellipse.VerticalAlignmentProperty, VerticalAlignment.Center);
    • Notes:
      I quickly discovered, thanks to the compiler, that Height, Width and StrokeThickness need to have values of type Double so I simply added the decimal point to squash those errors. As for the bindings, this time I went for in-line declarations. I think that looks more organized.
  • Completing the Template setter
    • XAML:
      <Setter Property="Template">
       <Setter.Value>
           <ControlTemplate TargetType="{x:Type Button}">
               <Grid Width="60" Height="50">
                   ...
               </Grid>
           </ControlTemplate>
       </Setter.Value>
      </Setter>
    • C#:
      Setter setter4 = new Setter();
      setter4.Property = Button.TemplateProperty;
      
      ControlTemplate ct = new ControlTemplate(typeof(Button));
      
      FrameworkElementFactory grid = new FrameworkElementFactory(typeof(Grid));
      grid.SetValue(Grid.WidthProperty, 60.0);
      grid.SetValue(Grid.HeightProperty, 50.0);
      
      ...
      
      grid.AppendChild(TB);
      grid.AppendChild(CP);
      
      ct.VisualTree = grid;
      
      setter4.Value = ct;
      DigitBtn.Setters.Add(setter4);
    • Notes:
      Because I was going to have to separate this setter's Value property, I used the empty constructor and then acted on the instance. Setting the setter's property property (that was fun to write) was as straight forward as the others. To set the setter's value property, I first created a ControlTemplate targeting a Button. Then I created a grid to hold the Ellipse and ContentPresenter. After instantiating the object to go into the grid, I discovered that I couldn't treat this grid object like I have before because it's actually a FrameworkElementFactory object. It was easy to find out how to add children to it though. Finally, thanks to Douglass Stockwell, I knew how to use VisualTree to complete the setter.
  • Adding the style to the main grid
    • XAML:
      <Grid.Resources >
       ...
       </Style>
      </Grid.Resources>
    • C#:
      MyGrid.Resources.Add("DigitBtn", DigitBtn);
    • Notes:
      The style is defined inside the "Grid.Resources" tag so I added the style to MyGrid.Resources collection in the same way as adding the storyboard to the Window.Resources collection of the Alarm Clock Sample.

Now that I got the style set up, I moved on to rendering the buttons. This part is just copy/paste once you get the first button to show. (In fact, I think it's so copy/paste that I might rewrite it using a loop.)

It was a little tricky for me because I haven't dealt with this level of customized UI before (at least with C#. I have done a complex GUI in Python with TkInter but TkInter's grid and pack managers are much less sophisticated).

  • Initializing the grid
    • XAML:
         <Grid.ColumnDefinitions>
           <ColumnDefinition/>
           ...
           <ColumnDefinition/>
         </Grid.ColumnDefinitions>
         <Grid.RowDefinitions>
           <RowDefinition/>
           ...
           <RowDefinition/>
         </Grid.RowDefinitions>
    • C#:
      MyGrid.ColumnDefinitions.Add(new ColumnDefinition());
      ...
      MyGrid.RowDefinitions.Add(new RowDefinition());
    • Notes:
      I tried rendering a button without this and it just overlapped with the DisplayBox. The grid geometry manager in TkInter automatically creates the cells and I assumed WPF grids worked the same. I saw all this XAML which did nothing but create default objects and thought, the code doesn't need this, but I was wrong. You explicitly have to create the rows and columns first, otherwise every visual object will overlap even if you've told the grid to put it somewhere else.
  • Creating the buttons
    • XAML:
      <Button Name="B7" Click="DigitBtn_Click" Style="{StaticResource DigitBtn}" Grid.Column="4" Grid.Row="2">7</Button>
      ...
    • C#:
      Button B7 = new Button();
      B7.Name = "B7";
      B7.Click += new RoutedEventHandler(DigitBtn_Click);
      B7.Style = MyGrid.Resources["DigitBtn"] as Style;
      Grid.SetColumn(B7, 4);
      Grid.SetRow(B7, 2);
      B7.Content = "7";
      MyGrid.Children.Add(B7);
    • Notes:
      It all made sense until the little "7" stuck between the tags. I looked up how to get the text for a button to show up and that led me to the wrong Button Class. It was telling me to set the Button.Text property but the compiler told me it doesn't exist. Someone else said, "WPF C# .text not working" and later said they were being an idiot because one should set the Button.Content property. I went back to the .NET Framework Class Library and discovered that I, too, was being an idiot because the correct place to look is the System.Windows.Controls.Button Class which does, indeed, have a Button.Content property. Oh, and also note "as Style" when grabbing the Style resource. In the Alarm Clock Sample, the storyboard resource is grabbed with a cast (Storyboard)clockWindow.Resources["clockHandStoryboard"]. The big difference between using a cast and using "as" is a cast will raise an exception but "as" will just yield null.

I then copied the DigitBtn_Click method into my Window1 class and now my unfinished calculator is at least looking much more finished. (I still don't know why they are converting the string into a character array instead of indexing the string itself...)

I planned on animating the buttons in this installment but just getting the buttons to render correctly was enough work for this time. I promise to do the animations next. I may even add in the operations.

0 comments:

Post a Comment