Alarm Clock Sample — Part 5

I left off saying, I would "rewrite the Alarm Clock Sample in pure C# without help from XAML". This turned out to be much more difficult than expected. XAML hides a lot of complexity so about 80% of the time it took to rewrite it was spent looking up references and examples in the .NET Class Framework Library and other sources. It seems WPF was developed with XAML in mind and so the amount of resources which explain how to use it with C# are severely lacking. I put the source on my github account.

Firstly, every C# program must have a Main() method as an entry point. I added one to a new myapp.cs (without the "partial" modifier for the class definition) and tried to compile. It wouldn't. For some reason it was complaining about missing references but I put in the correct "using" statements and even tried copying and pasting code that was supposed to work from both Microsoft and Hello WPF (without XAML). I commented out the body of the "AppStartup" method since I didn't care about actually getting the clock to work. I just wanted to start the application. Eventually, I found Building a WPF Application by Using Command-Line Compilation which shows there are three references which do not get added by default and must be available for WPF applications. I didn't feel like typing all that in so I fired up Visual Studio and added those references. Sure enough, it compiled and ran (but could only be seen in the task manager of course).

Second, I created traditionalclock.cs so I could get at least the clock image to show in a transparent window. I copied traditionalclock.xaml.cs and commented everything except the constructor. From part 3, I knew that InitializeComponent(); was for parsing the relevant XAML so I removed it and began to look at traditionalclock.xaml.

  • Rendering the window
    • XAML:
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      Name="clockWindow"
      AllowsTransparency="true"
      Background="Transparent"
      WindowStyle="None"
      MouseLeftButtonDown="LeftButtonDown"
      >...
    • C#:
          this.Name = "clockWindow";
          this.AllowsTransparency=true;
          this.Background=Brushes.Transparent;
          this.WindowStyle=WindowStyle.None;
          this.MouseLeftButtonDown += new System.Windows.Input.MouseButtonEventHandler(this.LeftButtonDown);
    • Notes:

      This was the easiest code to implement because the XAML code is nearly in one-to-one correspondence to the required C# code. Notice the "MouseButtonEventHandler" comes from a different namespace than all the "using" statements.

  • Rendering the image
    • XAML:
          <Canvas Name="clockCanvas" Width="292" Height="493"  >
          ...
          <Image Source="TradClock.png" Loaded="SetTime" />
          ...
          </Canvas>
    • C#:
      using System.Windows.Media.Imaging;
          ...
          Canvas clockCanvas = new Canvas();
          ...
          clockCanvas.Name = "clockCanvas";
          clockCanvas.Width = 292;
          clockCanvas.Height = 493;
          ...
          Image tradClock = new Image(); 
          BitmapImage bi3 = new BitmapImage();
          bi3.BeginInit();
          bi3.UriSource = new Uri("TradClock.png", UriKind.Relative);
          bi3.EndInit();
          tradClock.Source = bi3;
          tradClock.Loaded += new RoutedEventHandler(this.SetTime);
          clockCanvas.Children.Add(tradClock);
          ...
          this.Content = clockCanvas;
    • Notes:

      I initialized "clockCanvas" as a class variable so that it could be referenced outside the contructor. Setting properties of the canvas was quite simple by looking at the Canvas Class documentation but placing the image onto the canvas was more difficult. The XAML makes it look like you can just create a new instance of an Image and then set the source to be the file. It's not as straight forward as that but, fortunately, Image.Source Property gives some helpful details. It doesn't, however, tell you you must include the System.Windows.Media.Imaging namespace. Lastly, notice the image must be made a child of the canvas and the canvas must be put into the window's content. This is analogous to nesting the tags in XAML.

When I was porting the XAML over to C#, I actually just started from the top and continued downward. But that wasn't good for two reasons: one, the animations require targets which wouldn't be created until towards the end; two, since I didn't understand how everything fit together, when there were errors it was difficult to figure out what was going wrong. Above are the parts which I didn't have to comment out when I decided I should just try to get the clock to show up. Once I finished the above, the image appeared in a transparent window and I was able to continue. Since getting the image to render was much more involved than the XAML led me to believe, I set out to get the hands of the clock to render before trying the animation. (Each hand is basically the same, so I will only consider the hour hand)

  • Initializing the clock hands (make the shape)
    • XAML:
          <"hourHand" Canvas.Top="214" Canvas.Left="173" Points="0,5 3,0 4,0 8,5 8,50 0,50">...
    • C#:
          Polygon hourHand = new Polygon();
          Point hPoint1 = new Point(0, 5);
          Point hPoint2 = new Point(3, 0);
          Point hPoint3 = new Point(4, 0);
          Point hPoint4 = new Point(8, 5);
          Point hPoint5 = new Point(8, 50);
          Point hPoint6 = new Point(0, 50);
          PointCollection myPointCollection = new PointCollection();
          myPointCollection.Add(hPoint1);
          myPointCollection.Add(hPoint2);
          myPointCollection.Add(hPoint3);
          myPointCollection.Add(hPoint4);
          myPointCollection.Add(hPoint5);
          myPointCollection.Add(hPoint6);
          hourHand.Points = myPointCollection;
          ...
          hourHand.Name = "hourHand";
          ...
          Canvas.SetTop(hourHand, 214);
          Canvas.SetLeft(hourHand, 173);
          clockCanvas.Children.Add(hourHand);
    • Notes:

      I wasn't sure how to get that list of points to become a property of the Polygon shape. Visual Studio was telling me "Polygon.Points" required a collection of points but did that mean an array or some other kind of object? Luckily, there is a great example at the Polygon.Points Property reference. It is possible, and more traditional, to make an array of points which you can iterate over to add each point to the collection in turn:

          Point[] hPoints = new Point[6] { new Point(0, 5), new Point(3, 0), new Point(4, 0), 
          new Point(8, 5), new Point(8, 50), new Point(0, 50)};
          PointCollection myPointCollection = new PointCollection();
          foreach (Point p in hPoints){
          myPointCollection.Add(p);
          }
      and, even better, there's a overload of the "PointCollection" constructor which takes an iterable collection of points to automatically add them so you can just write PointCollection myPointCollection = new PointCollection(hPoints); without writing the loop. I just did each point explicitly to stay true to the example Microsoft gives in the documentation.
      XAML was misleading again with "Canvas.Top" and "Canvas.Left" but the documentation cleared everything up for me.

  • Rendering the clock hands (make it visible)
    • XAML:
          <polygon.fill>
          <LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
          <lineargradientbrush.gradientstops>
          <gradientstop offset="0" color="White"></gradientstop>
          <gradientstop offset="1" color="DarkGray"></gradientstop>
          </lineargradientbrush.gradientstops>
          </polygon.fill>
    • C#:
          LinearGradientBrush hourHandBrush = new LinearGradientBrush();
          hourHandBrush.StartPoint = new Point(0, 0);
          hourHandBrush.EndPoint = new Point(1, 0);
          hourHandBrush.GradientStops.Add(new GradientStop(Colors.White, 0));
          hourHandBrush.GradientStops.Add(new GradientStop(Colors.DarkGray, 1));
          hourHand.Fill = hourHandBrush;
    • Notes:

      I looked at the Shape.Fill Property first because that is what is first in XAML. It gave me a good starting point because I noticed they created a new instance of a "SolidColorBrush", set the properties of the brush and then set the brush as the fill property. All, I had to do was look up how to instantiate the properties of a new LinearGradientBrush and then follow the example.

The "<!--Center circles-->" were extremely easy once I got all the hands set up. In the next part, I will show how I got the animations going.

No comments:

Post a Comment