Blog Archives

OpenTK, entering the C# 3D world

I was amazed how short the source code can be to show complex moving objects. It does take a bit of research and the OpenTK library though. There are a lot of very good YouTube tutorials out there. Not necessarily about OpenTK, but plenty about OpenGL, which is what OpenTK is using.

I asked my daughters to draw some Minecraft style avatars with a simple icon editor. They had their fun and I got something to display in 3D. I stored three icon files (Freya.ico, Merlin.ico and Steve.ico) in a subfolder called ‘Resources’.

OpenTk

Change the file properties. ‘Build Action’ should be set to ‘None’, because you don’t have to compile these files. And you don’t want to copy the files each time you run the compiler. Simply set ‘Copy to Output Directory’ to ‘Copy if newer’.

OpenTk2

The next step is to install and reference the OpenTK library (OpenTK, OpenTK.Compatibility and OpenTK.GLControl). Be aware that there is no real WPF support for OpenTK. You only host a Winform control within WPF. Therefore you should also reference “WindowsFormsIntegration”.
You can also open a pure OpenTK window. But it is impossible to add further WPF controls to the same window then. We are far away from programming games, so it is always a good option to leave the backdoor open for further WPF. You might want to add sliders to set angles and object positions.

 

OpenTk3

 

Sorry for posting less for a while. I am working like a dog – leaving home at 6am and returning around 10pm during the week.

 

<Window x:Class="OpenTkControl.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:WinF="clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms"
        xmlns:OpenTK="clr-namespace:OpenTK;assembly=OpenTK.GLControl"
        Title="OpenTK Demo" Height="600" Width="800">
  <DockPanel LastChildFill="True">
    <WindowsFormsHost  x:Name="WinFormsContainer" Background="Transparent" DockPanel.Dock="Top"  >
      <OpenTK:GLControl x:Name="OpenTkControl" 
                        Paint="OpenTkControl_Paint" Dock="Fill" />
    </WindowsFormsHost>
  </DockPanel>
</Window>
using OpenTK;
using OpenTK.Graphics.OpenGL;
using System;
using System.Drawing;
using System.Windows;
using System.Windows.Threading;

namespace OpenTkControl {
  public partial class MainWindow : Window {

    private DispatcherTimer _Timer;
    private DateTime _ProgramStartTime;

    public MainWindow() {
      InitializeComponent();

      _ProgramStartTime = DateTime.Now;

      _Timer = new DispatcherTimer(DispatcherPriority.Send);
      _Timer.IsEnabled = true;
      _Timer.Interval = new TimeSpan(0, 0, 0, 0, 30);
      _Timer.Tick += OnTimer;
      _Timer.Start();
    } // constructor

    void OnTimer(object sender, EventArgs e) {
      OpenTkControl.Invalidate();
    } //

    private void OpenTkControl_Paint(object sender, System.Windows.Forms.PaintEventArgs e) {
      GLControl lControl = OpenTkControl;

      // Reset the depth and color buffer.
      // We want to render a new world. We do not want to continue with a previous rendering.
      GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

      // Create a projection matrix transforming camera space to raster space. (google for "view frustum")
      // Which is like: Press the 3D world and make it flat like a pancake, so that it does fit on the 2D screen.
      //                All points between a distance of 1 and 1000 will be taken into account.
      float lAngleView = 1.2f;              // y direction (in radians)
      float lAspectRatio = 4f / 3f;         // width / height
      float lDistanceToNearClipPlane = 1f;
      float lDistanceToFarClipPlane = 1000f;
      Matrix4 lPerspective = Matrix4.CreatePerspectiveFieldOfView(lAngleView, lAspectRatio, lDistanceToNearClipPlane, lDistanceToFarClipPlane);
      GL.MatrixMode(MatrixMode.Projection); GL.LoadIdentity(); GL.LoadMatrix(ref lPerspective);

      // camera setup
      Vector3 lCameraLocation = new Vector3(100f, 10f, 0f);
      Vector3 lCameraLookingAt = new Vector3(0f, 0f, 0f);     // look at the center of the coordinate system
      Vector3 lCameraWhatIsUpside = new Vector3(0f, 1f, 0f);  // classical way to hold a camera
      Matrix4 lCamera = Matrix4.LookAt(lCameraLocation, lCameraLookingAt, lCameraWhatIsUpside);
      GL.MatrixMode(MatrixMode.Modelview); GL.LoadIdentity(); GL.LoadMatrix(ref lCamera);

      // this is the size on the screen
      GL.Viewport(0, 0, lControl.Width, lControl.Height);

      // only draw the nearest pixels and not pixels that are actually hidden by other pixels 
      GL.Enable(EnableCap.DepthTest);
      GL.DepthFunc(DepthFunction.Less);

      // set time dependent variables to generate movements
      double lTotalMillis = DateTime.Now.Subtract(_ProgramStartTime).TotalMilliseconds;
      double lTime1 = (lTotalMillis % 10000.0) / 10000.0; // between 0 and 1
      double lTime2 = (lTotalMillis % 2000.0) / 2000.0;   // between 0 and 1
      double lTimeRadians = lTime2 * 2.0 * Math.PI;
      float lJump = (float)(-20.0 + 10.0 * Math.Sin(lTimeRadians));
      float lRadius = -40f;

      // add the comet
      DrawComet(lTotalMillis);

      // render the floor
      GL.Rotate(360.0 * lTime1, 0.0, 1.0, 0.5);  // rotate around y axis and half as much around z axis
      DrawFloor();

      // render objects
      // from where we are; now rotate the objects into the opposite direction
      GL.Rotate(-lTime1 * 360.0 * 2.0, 0.0, 1.0, 0.0); DrawAvatar("Merlin", -30f, lRadius);
      GL.Rotate(60.0, 0.0, 1.0, 0.0); DrawAvatar("Freya", lJump, lRadius);
      GL.Rotate(60.0, 0.0, 1.0, 0.0); DrawAvatar("Steve", -30f, lRadius);
      GL.Rotate(60.0, 0.0, 1.0, 0.0); DrawAvatar("Merlin", lJump, lRadius);
      GL.Rotate(60.0, 0.0, 1.0, 0.0); DrawAvatar("Freya", -30f, lRadius);
      GL.Rotate(60.0, 0.0, 1.0, 0.0); DrawAvatar("Steve", lJump, lRadius);

      // render the cube in the center
      //GL.Rotate(360f * lTime2, 0f, 0f, 0f); // <= this kind of rotation lets the box bounce and change its size
      DrawCube(Color.SteelBlue, Color.DarkBlue, 0f, -25f, 0f, 8f, false);

      OpenTK.Graphics.GraphicsContext.CurrentContext.VSync = true; // caps GPU frame rate
      lControl.SwapBuffers();  // display our newly generated buffer with all objects
    } //

    private void DrawAvatar(string xName, float yShift, float zShift) {
      Icon lIcon = new Icon("Resources/" + xName + ".ico");
      Bitmap lBitmap = lIcon.ToBitmap();
      int lWidth = lBitmap.Width; float lHalfWidth = lWidth / 2f;
      int lHeight = lBitmap.Height; float lHalfHeight = lHeight;
      for (int y = 0; y < lHeight; y++) {
        for (int x = 0; x < lWidth; x++) {
          Color lColor = lBitmap.GetPixel(x, y);
          if (lColor.A != 0) DrawCube(lColor, lColor, (float)x - lHalfWidth, lHeight + yShift - (float)y, (float)zShift, 1f, true);
        }
      }
    } //

    private void DrawFloor() {
      for (int x = -100; x < 100; x += 10) {
        for (int z = -100 + (x % 10 == 0 ? 5 : 0); z < 100; z += 10) {
          DrawCube(Color.White, Color.Gray, x, -30f, z, 5f, false);
        }
      }
    } //

    private void DrawComet(double xTotalMillis) {
      xTotalMillis = (xTotalMillis % 7000.0) / 7000.0; // between 0 and 1

      GL.PushMatrix();
      GL.LoadIdentity();
      GL.Translate(xTotalMillis * 30f - 40f , 40f,  400f * xTotalMillis - 400f);
      GL.Rotate(360f * xTotalMillis * 3f, 1f, 1f, 1f);
      DrawTetrahedron(Color.Orange, Color.OrangeRed, 0f, 0f, 0f, 8f);
      GL.Rotate(180f, 1f, 0f, 0f);
      DrawTetrahedron(Color.Orange, Color.OrangeRed, 0f, 0f, 0f, 8f);
      GL.PopMatrix();
    } //

    private void DrawCube(System.Drawing.Color xColor, System.Drawing.Color xColor2, float X, float Y, float Z, float xWidth, bool xHasDarkBack) {
      float lHalfWidth = xWidth / 2f;
      float lTop = Y + lHalfWidth;
      float lBottom = Y - lHalfWidth;
      float lLeft = X - lHalfWidth;
      float lRight = X + lHalfWidth;
      float lFront = Z + lHalfWidth;
      float lRear = Z - lHalfWidth;

      GL.Begin(PrimitiveType.Quads);

      Color lColor; if (xHasDarkBack) lColor = Color.DarkGray; else lColor = xColor;
      Color lColor2; if (xHasDarkBack) lColor2 = Color.DarkGray; else lColor2 = xColor2;

      Action lPointFrontTopLeft = () => { GL.Color3(xColor); GL.Vertex3(lLeft, lTop, lFront); };
      Action lPointFrontTopRight = () => { GL.Color3(xColor2); GL.Vertex3(lRight, lTop, lFront); };
      Action lPointFrontBottomLeft = () => { GL.Color3(xColor2); GL.Vertex3(lLeft, lBottom, lFront); };
      Action lPointFrontBottomRight = () => { GL.Color3(xColor2); GL.Vertex3(lRight, lBottom, lFront); };
      Action lPointRearTopLeft = () => { GL.Color3(lColor); GL.Vertex3(lLeft, lTop, lRear); };
      Action lPointRearTopRight = () => { GL.Color3(lColor2); GL.Vertex3(lRight, lTop, lRear); };
      Action lPointRearBottomLeft = () => { GL.Color3(lColor2); GL.Vertex3(lLeft, lBottom, lRear); };
      Action lPointRearBottomRight = () => { GL.Color3(lColor2); GL.Vertex3(lRight, lBottom, lRear); };

      // front square
      lPointFrontTopLeft(); lPointFrontTopRight(); lPointFrontBottomRight(); lPointFrontBottomLeft();

      // rear square
      lPointRearTopLeft(); lPointRearTopRight(); lPointRearBottomRight(); lPointRearBottomLeft();

      // top square
      lPointFrontTopLeft(); lPointFrontTopRight(); lPointRearTopRight(); lPointRearTopLeft();

      // bottom square
      lPointFrontBottomLeft(); lPointFrontBottomRight(); lPointRearBottomRight(); lPointRearBottomLeft();

      // left square
      lPointFrontTopLeft(); lPointRearTopLeft(); lPointRearBottomLeft(); lPointFrontBottomLeft();

      // right square
      lPointFrontTopRight(); lPointRearTopRight(); lPointRearBottomRight(); lPointFrontBottomRight();

      GL.End();
    } //

    private void DrawTetrahedron(System.Drawing.Color xColor, System.Drawing.Color xColor2, float X, float Y, float Z, float xSideLength) {
      float lDistMidToVertex = (float)Math.Sqrt(6.0) / 4f * xSideLength;
      float lDistMidToFloor = (float)Math.Sqrt(6.0) / 12f * xSideLength;
      float lHeight = (float)Math.Sqrt(2.0 / 3.0) * xSideLength; // = lDistMidToVertex + lDistMidToEdge
      float lTop = Y + lDistMidToVertex;
      float lBottom = Y - lDistMidToFloor;
      float lRight = X + xSideLength / 2f;
      float lLeft = X - xSideLength / 2f;
      float lRear = Z - (float) (xSideLength * Math.Sqrt(3.0) / 3.0);
      float lFront = Z + (float)(xSideLength * Math.Sqrt(3.0) / 6.0);

      GL.Begin(PrimitiveType.Triangles);

      Action lPointTop = () => { GL.Color3(xColor); GL.Vertex3(X, lTop, Z); };
      Action lPointFrontBottomLeft = () => { GL.Color3(xColor2); GL.Vertex3(lLeft, lBottom, lFront); };
      Action lPointFrontBottomRight = () => { GL.Color3(xColor); GL.Vertex3(lRight, lBottom, lFront); };
      Action lPointRear = () => { GL.Color3(xColor2); GL.Vertex3(X, lBottom, lRear); };

      // front triangle
      lPointTop(); lPointFrontBottomLeft(); lPointFrontBottomRight();

      // left triangle
      lPointTop(); lPointFrontBottomLeft(); lPointRear();

      // right triangle
      lPointTop(); lPointFrontBottomRight(); lPointRear();

      // bottom triangle
      lPointFrontBottomLeft(); lPointFrontBottomRight(); lPointRear();

      GL.End();
    } //

  } // class
} // namespace

 

Slogan / News Ticker / Scrolling Text in WPF (Part 2)

 

This is the follow-up of my last post.

I promised to enhance the scroll behavior. Here it is. You can influence the text in various ways. When the zoom factor turns negative you can even see upside down characters. Also the drift can turn negative; if so the wave moves to the left, but its peaks drift to the right.
Weirdly enough it is a lot of fun playing with the sliders and watching the wave change. Of course, we could still create far more complex movements. This source code here is providing the full basics. The rest is up to you.

What you can influence:

  • text zoom  (negative numbers turn the text upside down)
  • scroll speed
  • wave amplitude
  • wave drift
  • wave length

I had to slightly change the way the vectors are stored to enable zooming. The y-zero-line now is in the middle of the characters and not at the top anymore. Anything above that line is negative, anything below positive. Hence the vectors can be multiplied with a zoom factor without the need to recalculate the position in the WPF control.

<Window x:Class="Ticker.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Ticker"
        Height="250"
        Width="500"
        Loaded="Window_Loaded">
  <DockPanel x:Name="MyDockPanel"
             SizeChanged="MyImage_SizeChanged"
             LastChildFill="True">
    <Grid DockPanel.Dock="Top">
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="110"/>
        <ColumnDefinition Width="50*"/>
        <ColumnDefinition Width="110"/>
        <ColumnDefinition Width="50*"/>
      </Grid.ColumnDefinitions>
      <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
      </Grid.RowDefinitions>

      <Label Content="calculation speed" Grid.Row="0"
             HorizontalAlignment="Right" Padding="0,0,0,0"/>
      <TextBox Name="Info"
               Grid.Row="0" Grid.Column="1"/>

      <Label Content="zoom" Grid.Row="0" Grid.Column="2"
             HorizontalAlignment="Right" Padding="0,0,0,0"/>
      <Slider Minimum="-1.0" Maximum="1.0" Value="1.0"
              ValueChanged="Slider_Zoom_ValueChanged"
              Grid.Row="0" Grid.Column="3" />

      <Label Content="scroll speed" Grid.Row="1" Grid.Column="0"
             HorizontalAlignment="Right" Padding="0,0,0,0"/>
      <Slider Minimum="0.5" Maximum="8.0" Value="2.0"
              ValueChanged="Slider_ScrollSpeed_ValueChanged"
              Grid.Row="1" Grid.Column="1" />

      <Label Content="wave amplitude" Grid.Row="1" Grid.Column="2"
             HorizontalAlignment="Right" Padding="0,0,0,0"/>
      <Slider Minimum="0.0" Maximum="100.0" Value="50.0"
              ValueChanged="Slider_Amplitude_ValueChanged"
              Grid.Row="1" Grid.Column="3" />

      <Label Content="wave drift" Grid.Row="2" Grid.Column="0"
             HorizontalAlignment="Right" Padding="0,0,0,0"/>
      <Slider Minimum="-10.0" Maximum="10.0" Value="5.0"
              ValueChanged="Slider_Drift_ValueChanged"
              Grid.Row="2" Grid.Column="1" />

      <Label Content="wave length" Grid.Row="2" Grid.Column="2"
             HorizontalAlignment="Right" Padding="0,0,0,0"/>
      <Slider Minimum="0.0" Maximum="2.0" Value="0.5"
              ValueChanged="Slider_WaveLength_ValueChanged"
              Grid.Row="2" Grid.Column="3" />
    </Grid>
    <Image Name="MyImage"
           DockPanel.Dock="Top"
           Stretch="None"
           Width="{Binding ActualWidth, ElementName=MyDockPanel}"/>
  </DockPanel>
</Window>
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Imaging;
using System.Windows.Threading;

namespace Ticker {

  public partial class MainWindow : Window {

    private Thread _Thread;
    private TextEngine _TextEngine = new TextEngine();
    private float _WidthInPixels;
    private double _Speed = 1.0; // number of pixels to shift per iteration
    private AutoResetEvent _AutoResetEvent = new AutoResetEvent(true);
    private BitmapImage _BitmapImage = null;
    private string _ElapsedTime = string.Empty;

    public MainWindow() {
      InitializeComponent();
    } //  constructor

    private void Window_Loaded(object sender, EventArgs e) {
      DataContext = this;

      _Thread = new Thread(Loop);
      _Thread.Name = "MainLoop";
      _Thread.IsBackground = true;
      _Thread.Priority = ThreadPriority.AboveNormal;
      _Thread.Start();

      TimeSpan lInterval = new TimeSpan(0, 0, 0, 0, 50); // run each 50 ms
      EventHandler lHandler = new EventHandler(OnTime);
      DispatcherTimer lDispatcherTimer = new DispatcherTimer(lInterval, DispatcherPriority.Send, lHandler, this.Dispatcher);
    } //

    private void MyImage_SizeChanged(object sender, SizeChangedEventArgs e) {
      DockPanel lDockPanel = sender as DockPanel;
      if (lDockPanel == null) return;
      using (var lGraphics = Graphics.FromHwnd(IntPtr.Zero)) {
        _WidthInPixels = (float)(e.NewSize.Width * lGraphics.DpiX / 96.0);
      }
    } //

    public void OnTime(object XSender, EventArgs e) {
      BitmapImage lBitmapImage = _BitmapImage;
      if (lBitmapImage == null) return;

      MyImage.Source = lBitmapImage;
      Line.adjustDrift();
      Info.Text = _ElapsedTime;
      _AutoResetEvent.Set();
    } //

    private void Loop() {
      float lEnd = 0f;
      int lSectionFrom = 0;
      int lSectionTo = 0;
      Stopwatch lStopwatch = new Stopwatch();

      while (true) {
        _AutoResetEvent.WaitOne();
        lStopwatch.Restart();

        float lWidthInPixel = _WidthInPixels; // copy the value to avoid changes during the calculation
        if (lWidthInPixel <= 0.0) continue;
        List<Line> lSection = _TextEngine.getVectorSection(ref lSectionFrom, ref lSectionTo, lEnd, lWidthInPixel);

        // This value determines the speed.
        // Even numbers give better results due to the rounding error nature of bitmaps.
        // Odd numbers create jitter. Luckily humans have bad eyes, they cannot perceive it.
        lEnd += (float)_Speed;

        if (lSection == null) {
          // end reached, reset text
          lSectionFrom = 0;
          lSectionTo = 0;
          lEnd = 0f;
        }
        else {
          Bitmap lBitmap = _TextEngine.VectorsToBitmap(lSection, lWidthInPixel);
          _BitmapImage = _TextEngine.BitmapToImageSource(lBitmap);
        }

        _ElapsedTime = lStopwatch.ElapsedMilliseconds.ToString("#,##0");
        //lStopwatch.Stop();
      }
    } //

    private void Slider_ScrollSpeed_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e) { _Speed = e.NewValue; }
    private void Slider_Amplitude_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e) { Line.Amplitude = (float)e.NewValue; }
    private void Slider_Drift_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e) { Line.Drift = (float)e.NewValue; }
    private void Slider_WaveLength_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e) { Line.WaveLength = (float)e.NewValue; }
    private void Slider_Zoom_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e) { Line.Zoom = (float)e.NewValue; }

  } // class
} // namespace
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Windows.Media.Imaging;

namespace Ticker {
  public class TextEngine {

    private const float cFontHeight = 40f;
    private const string cText = @"Die Gedanken sind frei, wer kann sie erraten, sie fliegen vorbei wie nächtliche Schatten. Kein Mensch kann sie wissen, kein Jäger erschießen mit Pulver und Blei: Die Gedanken sind frei! Ich denke was ich will und was mich beglücket, doch alles in der Still', und wie es sich schicket. Mein Wunsch und Begehren kann niemand verwehren, es bleibet dabei: Die Gedanken sind frei! Und sperrt man mich ein im finsteren Kerker, das alles sind rein vergebliche Werke. Denn meine Gedanken zerreißen die Schranken und Mauern entzwei: Die Gedanken sind frei! Drum will ich auf immer den Sorgen entsagen und will mich auch nimmer mit Grillen mehr plagen. Man kann ja im Herzen stets lachen und scherzen und denken dabei: Die Gedanken sind frei! Ich liebe den Wein, mein Mädchen vor allen, sie tut mir allein am besten gefallen. Ich sitz nicht alleine bei meinem Glas Weine, mein Mädchen dabei: Die Gedanken sind frei!";
    private List<Line> _TextAsVectorChain = new List<Line>();
    private Font _Font = new Font("Arial", cFontHeight, System.Drawing.FontStyle.Bold, GraphicsUnit.Pixel);

    public TextEngine() {
      // convert the entire text to vectors
      float lPosition = 0;
      Dictionary<char, List<Line>> lVectorCache = new Dictionary<char, List<Line>>();
      char[] lChars = cText.ToCharArray();

      foreach (char lChar in lChars) {
        if (lChar == ' ') lPosition += 10; // distance for an empty space character
        else {
          List<Line> lOneCharVectors;

          if (!lVectorCache.TryGetValue(lChar, out lOneCharVectors)) {
            Bitmap lBitmap = CharToBitmap(lChar);
            lOneCharVectors = BitmapToVectors(lBitmap);
            lVectorCache.Add(lChar, lOneCharVectors);
          }

          float lNewPosition = lPosition;
          foreach (Line lLine in lOneCharVectors) {
            Line lClone = lLine.Clone();
            lClone.X += lPosition;
            lNewPosition = lClone.X;
            _TextAsVectorChain.Add(lClone);
          }
          lPosition = lNewPosition + 4; // 4 == space between two characters
        }

      }
    } // constructor

    // Convert a bitmap to an ImageSource.
    // We can then display the result in the WPF Image element.
    public BitmapImage BitmapToImageSource(Bitmap xBitmap) {
      using (MemoryStream lMemoryStream = new MemoryStream()) {
        xBitmap.Save(lMemoryStream, System.Drawing.Imaging.ImageFormat.Bmp);
        lMemoryStream.Position = 0;
        BitmapImage lBitmapImage = new BitmapImage();
        lBitmapImage.BeginInit();
        lBitmapImage.StreamSource = lMemoryStream;
        lBitmapImage.CacheOption = BitmapCacheOption.OnLoad;
        lBitmapImage.EndInit();
        lBitmapImage.Freeze();
        return lBitmapImage;
      }
    } //

    // draw a single character into a bitmap
    private Bitmap _Bitmap = null;
    public Bitmap CharToBitmap(char xChar) {
      _Bitmap = new Bitmap((int)(cFontHeight * 1.25f), (int)(cFontHeight * 1.25f));
      using (Graphics lGraphics = Graphics.FromImage(_Bitmap)) {
        lGraphics.Clear(Color.White);
        lGraphics.DrawString(xChar.ToString(), _Font, Brushes.Black, 0f, 0f);
      }
      return _Bitmap;
    } //

    // Replicate the characters now by reading the vectors and drawing lines.
    Pen lPen = new Pen(Color.Black, 2f);
    public Bitmap VectorsToBitmap(List<Line> xLines, float xBitmapWidth) {
      int lHeight = (int)cFontHeight + (int)Math.Abs(Line.Amplitude);
      Bitmap lBitmap = new Bitmap((int)xBitmapWidth, lHeight);
      lHeight /= 2;   // half height, rounded down
      using (Graphics lGraphics = Graphics.FromImage(lBitmap)) {
        lGraphics.Clear(Color.White);
        foreach (Line lLine in xLines) {
          lGraphics.DrawLine(lPen, lLine.X, lLine.Y1 + lHeight, lLine.X, lLine.Y2 + lHeight);
        }
      }
      return lBitmap;
    } //

    // Convert a single character to vectors.
    private List<Line> BitmapToVectors(Bitmap xBitmap) {
      int lXCoordinateOfFirstPixel = -1;
      List<Line> lList = new List<Line>();

      for (int x = 0, lWidth = xBitmap.Width; x < lWidth; x++) {
        Line lVector = null;
        for (int y = 0, lHeight = xBitmap.Height; y < lHeight; y++) {
          Color lColor = xBitmap.GetPixel(x, y);
          bool lIsWhite = lColor.B == 255;
          if (lIsWhite) {
            if (lVector != null) {
              lList.Add(lVector);
              lVector = null;
            }
          }
          else {
            int lHalfHeight = xBitmap.Height / 2;
            if (lVector == null) {
              if (lXCoordinateOfFirstPixel < 0) lXCoordinateOfFirstPixel = x;  // to always start at zero for our vectors
              lVector = new Line { X = x - lXCoordinateOfFirstPixel, Y1 = y - lHalfHeight, Y2 = y - lHalfHeight };
            }
            else lVector.Y2 = y - lHalfHeight;
          }
        }
      }

      return lList;
    } //

    // The text was converted to vectors.
    // Now we cut out the sequence we need for the display.
    internal List<Line> getVectorSection(ref int xSectionFrom, ref int xSectionTo, float xEnd, float xWidth) {
      int lCount = _TextAsVectorChain.Count;
      float lStart = xEnd - xWidth;

      // find the right section
      do {
        xSectionTo++;
        if (xSectionTo >= lCount) { xSectionTo = lCount - 1; break; }
        if (xEnd < _TextAsVectorChain[xSectionTo].X) break;
      } while (true);

      do {
        if (lStart < 0) break; // to allow empty spaces at the beginning of the slogan
        if (xSectionFrom >= lCount) return null;
        if (lStart < _TextAsVectorChain[xSectionFrom].X) break;
        xSectionFrom++;
      } while (true);

      // clone that section
      List<Line> lList = new List<Line>();
      for (int x = xSectionFrom; x <= xSectionTo; x++) {
        Line lClone = _TextAsVectorChain[x].Clone();
        lClone.X -= lStart; // adjust the X-axis
        lClone.RecalcYShift();
        lList.Add(lClone);
      }

      return lList;
    } //

  } // class
} // namespace
using System;

namespace Ticker {
  public class Line {
    private float _Y1, _Y1Shift;
    private float _Y2, _Y2Shift;
    private static float _XDrift = 0f;

    public float X { get; set; }
    public static float Amplitude { get; set; }    // This is the additional height needed for the vertical swing
    public static float Drift { get; set; }        // How fast does the wave crest/trough move?
    public static float WaveLength { get; set; }   // How wide is the wave?
    public static float Zoom { get; set; }         // Character zoom factor around the horizontal middle.

    public static void adjustDrift() { _XDrift += Drift; }

    public float Y1 {
      get { return _Y1 * Zoom + _Y1Shift; }
      set { _Y1 = value; }
    } //

    public float Y2 {
      get { return _Y2 * Zoom + _Y2Shift; }
      set { _Y2 = value; }
    } //

    public void RecalcYShift() {
      double d = (double)(WaveLength * (X + _XDrift));
      d = Math.IEEERemainder(d / 50.0, 1.99999999999);
      float lAngle = (float)Math.Sin(Math.PI * d);          // 0.0 <= d < 2.0  therefore -1.0 <= lAngle <= 1.0
      float lShift = Amplitude / 2.0f * lAngle;
      _Y1Shift = lShift;
      _Y2Shift = lShift;
    } //

    public Line Clone() {
      Line lLine = new Line();
      lLine.X = X;
      lLine.Y1 = _Y1;
      lLine.Y2 = _Y2;
      return lLine;
    } //

  } // class
} // namespace

Slogan / News Ticker / Scrolling Text in WPF

Ticker

Oh, I used to write so called intros with scrolling text slogans at the bottom on the Commodore Amiga. The hardware was pretty much unalterable and you could easily work with the Blitter and Copper. The vertical blank beam on the monitor was my timer. These days are over. No more Assembler. And who cares about high and low byte these days?

I have to admit that using C# in combination with WPF is not the best approach for moving text around. You should directly talk to graphic cards for such. Anyway, it started with a short benchmark test and ended up with this example program. If you program properly, then you can make WPF do the job without any flickering.

You might wonder why my approach is quite indirect this time. I am not simply rendering text to the screen and then change its position. This program is slightly more complex. The reason is that I am planning a follow-up with more complex movements soon. I am planning to show you how to scroll text along the screen in a sinus curve while the curve itself behaves like a wave. Maybe I will add some topsy-turvy stuff as well. Hehe, it is not written yet.

This post is providing the basics to get prepared for the next step. Today, the text simply moves from the right to the left. And no worries about the end of the text. It is dealt with properly.
You can change the speed with the slider while the text field returns the calculation time in milliseconds. You can see that we still have some timing leeway. There are no issues with our frequency, which is 20 updates per second. The screen itself obviously updates more often. Its frequency has little to do with the one for showing new BitmapImages. Our thread loop takes less than 20 milliseconds.

How does the program work?

  • A loop is running on an independent thread. This loop executes each time right after our WPF update. This well chosen moment avoids timing conflicts efficiently. The next BitmapImage is ready to be shown before the next WPF GUI update.
  • At the beginning of the program we convert the entire text to vectors. The orientation of all vectors is vertical. Each x-value can consist of several vertical vectors. For instance the character “E” has 1 or 3 vertical vectors. The first vector is a straight vertical black line from the top to the bottom. When you are in the middle of the character, then you have three black and two white areas. We only convert the black parts. Imagine a vertical cut right through the middle from the top to the bottom.
  • The conversion from “character to bitmap to vector” is NOT conducted in real-time. This is done during the program initialization as mentioned above. Only then we can run at full steam afterwards.
  • We only change the precalculated vectors and render the bitmap in real-time – nothing else.
  • These vectors are then drawn to bitmaps. These bitmaps are not dependent on the Dispatcher thread. We are still running at maximum speed. Before we can change the source for the Image control, we have to convert the Bitmap class to an ImageSource.
  • This ImageSource is then assigned to the Image control during a DispatcherTimer event.

Once again the program was kept to a minimum length. Maybe I should have added some more classes. I hope I found the right length for this example program in the end.

This code was not straight forward. There had to be a learning curve … as usual. First, I tried some direct WPF binding. The timing was nearly out of control. So I gave up after a while. But I had some nice blog ideas in the meantime. We should enter the 3D world at some point, don’t you think so?
The 3D approach would most likely the best approach for our scrolling text as well. I expect the efficiency to be extreme. There are so many games with millions of mesh triangles rendered within milliseconds – this cannot be bad stuff. We could even use 3D features for showing 2D text.

This will surely require some profound research on my side. Don’t expect anything anytime soon. I am definitely not omniscient.

<Window x:Class="Ticker.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Ticker" 
        Height="130"
        Width="400"
        Loaded="Window_Loaded">
  <StackPanel x:Name="MyStackPanel"
              SizeChanged="MyImage_SizeChanged">
    <DockPanel LastChildFill="True">
      <Slider Width="300"  DockPanel.Dock="Right" 
              Minimum="0.5" Maximum="8.0"
              Value="2.0"
              ValueChanged="Slider_ValueChanged" />
      <TextBox Name="Info"  DockPanel.Dock="Right"/>
    </DockPanel>
    <Image Name="MyImage" 
           Stretch="None"
           Height="60" 
           Width="{Binding ActualWidth, ElementName=MyStackPanel}"/>
  </StackPanel>
</Window>
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media.Imaging;
using System.Windows.Threading;

namespace Ticker {

  public partial class MainWindow : Window {

    private Thread _Thread;
    private TextEngine _TextEngine = new TextEngine();
    private float _WidthInPixels;
    private double _Speed = 1.0; // number of pixels to shift per iteration
    private AutoResetEvent _AutoResetEvent = new AutoResetEvent(true);
    private BitmapImage _BitmapImage = null;
    private string _ElapsedTime = string.Empty;

    public MainWindow() {
      InitializeComponent();
    } //  constructor

    private void Window_Loaded(object sender, EventArgs e) {
      DataContext = this;

      _Thread = new Thread(Loop);
      _Thread.Name = "MainLoop";
      _Thread.IsBackground = true;
      _Thread.Priority = ThreadPriority.AboveNormal;
      _Thread.Start();

      TimeSpan lInterval = new TimeSpan(0, 0, 0, 0, 50); // run each 50 ms
      EventHandler lHandler = new EventHandler(OnTime);
      DispatcherTimer lDispatcherTimer = new DispatcherTimer(lInterval, DispatcherPriority.Send, lHandler, this.Dispatcher);
    } //

    private void MyImage_SizeChanged(object sender, SizeChangedEventArgs e) {
      StackPanel lStackPanel = sender as StackPanel;
      if (lStackPanel == null) return;
      using (var lGraphics = Graphics.FromHwnd(IntPtr.Zero)) {
        _WidthInPixels = (float)(e.NewSize.Width * lGraphics.DpiX / 96.0);
      }
    } //

    public void OnTime(object XSender, EventArgs e) {
      BitmapImage lBitmapImage = _BitmapImage;
      if (lBitmapImage == null) return;

      MyImage.Source = lBitmapImage;
      Info.Text = _ElapsedTime;
      _AutoResetEvent.Set();
    } //

    private void Loop() {
      float lEnd = 0f;
      int lSectionFrom = 0;
      int lSectionTo = 0;
      Stopwatch lStopwatch = new Stopwatch();

      while (true) {
        _AutoResetEvent.WaitOne();
        lStopwatch.Restart();

        float lWidthInPixel = _WidthInPixels; // copy the value to avoid changes during the calculation
        if (lWidthInPixel <= 0.0) continue;
        List<Line> lSection = _TextEngine.getVectorSection(ref lSectionFrom, ref lSectionTo, lEnd, lWidthInPixel);

        // This value determines the speed. 
        // Even numbers give better results due to the rounding error nature of bitmaps. 
        // Odd numbers create jitter. Luckily humans have bad eyes, they cannot perceive it.
        lEnd += (float)_Speed;

        if (lSection == null) {
          // end reached, reset text
          lSectionFrom = 0;
          lSectionTo = 0;
          lEnd = 0f;
        }
        else {
          Bitmap lBitmap = _TextEngine.VectorsToBitmap(lSection, lWidthInPixel);
          _BitmapImage = _TextEngine.BitmapToImageSource(lBitmap);
        }

        _ElapsedTime = lStopwatch.ElapsedMilliseconds.ToString("#,##0");
        lStopwatch.Stop();
      }
    } //

    private void Slider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e) {
      _Speed = e.NewValue;
    } //

  } // class
} // namespace
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Windows.Media.Imaging;

namespace Ticker {
  public class TextEngine {

    private const string cText = @"Die Gedanken sind frei, wer kann sie erraten, sie fliegen vorbei wie nächtliche Schatten. Kein Mensch kann sie wissen, kein Jäger erschießen mit Pulver und Blei: Die Gedanken sind frei! Ich denke was ich will und was mich beglücket, doch alles in der Still', und wie es sich schicket. Mein Wunsch und Begehren kann niemand verwehren, es bleibet dabei: Die Gedanken sind frei! Und sperrt man mich ein im finsteren Kerker, das alles sind rein vergebliche Werke. Denn meine Gedanken zerreißen die Schranken und Mauern entzwei: Die Gedanken sind frei! Drum will ich auf immer den Sorgen entsagen und will mich auch nimmer mit Grillen mehr plagen. Man kann ja im Herzen stets lachen und scherzen und denken dabei: Die Gedanken sind frei! Ich liebe den Wein, mein Mädchen vor allen, sie tut mir allein am besten gefallen. Ich sitz nicht alleine bei meinem Glas Weine, mein Mädchen dabei: Die Gedanken sind frei!";
    private List<Line> _TextAsVectorChain = new List<Line>();

    public TextEngine() {
      // convert the entire text to vectors
      float lPosition = 0;
      Dictionary<char, List<Line>> lVectorCache = new Dictionary<char, List<Line>>();
      char[] lChars = cText.ToCharArray();

      foreach (char lChar in lChars) {
        if (lChar == ' ') lPosition += 10; // distance for an empty space character
        else {
          List<Line> lOneCharVectors;

          if (!lVectorCache.TryGetValue(lChar, out lOneCharVectors)) {
            Bitmap lBitmap = CharToBitmap(lChar);
            lOneCharVectors = BitmapToVectors(lBitmap);
            lVectorCache.Add(lChar, lOneCharVectors);
          }

          float lNewPosition = lPosition;
          foreach (Line lLine in lOneCharVectors) {
            Line lClone = lLine.Clone();
            lClone.X += lPosition;
            lNewPosition = lClone.X;
            _TextAsVectorChain.Add(lClone);
          }
          lPosition = lNewPosition + 4; // 4 == space between two characters
        }

      }
    } // constructor


    // Convert a bitmap to an ImageSource.
    // We can then display the result in the WPF Image element.
    public BitmapImage BitmapToImageSource(Bitmap xBitmap) {
      using (MemoryStream lMemoryStream = new MemoryStream()) {
        xBitmap.Save(lMemoryStream, System.Drawing.Imaging.ImageFormat.Bmp);
        lMemoryStream.Position = 0;
        BitmapImage lBitmapImage = new BitmapImage();
        lBitmapImage.BeginInit();
        lBitmapImage.StreamSource = lMemoryStream;
        lBitmapImage.CacheOption = BitmapCacheOption.OnLoad;
        lBitmapImage.EndInit();
        lBitmapImage.Freeze();
        return lBitmapImage;
      }
    } //

    // draw a single character into a bitmap
    private Font _Font = null;
    private Bitmap _Bitmap = null;
    public Bitmap CharToBitmap(char xChar) {
      if (_Font == null) {
        _Font = new Font("Arial", 40.0f, System.Drawing.FontStyle.Bold, GraphicsUnit.Pixel);
        _Bitmap = new Bitmap(60, 70);
      }
      using (Graphics lGraphics = Graphics.FromImage(_Bitmap)) {
        lGraphics.Clear(Color.White);
        lGraphics.DrawString(xChar.ToString(), _Font, Brushes.Black, 0f, 0f);
      }
      return _Bitmap;
    } //

    // Replicate the characters now by reading the vectors and drawing lines.
    Pen lPen = new Pen(Color.Black, 2f);
    public Bitmap VectorsToBitmap(List<Line> xLines, float xBitmapWidth) {
      if (_Font == null) { _Font = new Font("Arial", 40.0f, System.Drawing.FontStyle.Bold, GraphicsUnit.Pixel); }
      Bitmap lBitmap = new Bitmap((int)xBitmapWidth, 60);
      using (Graphics lGraphics = Graphics.FromImage(lBitmap)) {
        lGraphics.Clear(Color.White);
        foreach (Line lLine in xLines) {
          lGraphics.DrawLine(lPen, lLine.X, lLine.Y1, lLine.X, lLine.Y2);
        }
      }
      return lBitmap;
    } //

    // Convert a single character to vectors.
    private List<Line> BitmapToVectors(Bitmap xBitmap) {
      int lXCoordinateOfFirstPixel = -1;
      List<Line> lList = new List<Line>();

      for (int x = 0, lWidth = xBitmap.Width; x < lWidth; x++) {
        Line lVector = null;
        for (int y = 0, lHeight = xBitmap.Height; y < lHeight; y++) {
          Color lColor = xBitmap.GetPixel(x, y);
          bool lIsWhite = lColor.B == 255;
          if (lIsWhite) {
            if (lVector != null) {
              lList.Add(lVector);
              lVector = null;
            }
          }
          else {
            if (lVector == null) {
              if (lXCoordinateOfFirstPixel < 0) lXCoordinateOfFirstPixel = x;  // to always start at zero for our vectors
              lVector = new Line { X = x - lXCoordinateOfFirstPixel, Y1 = y, Y2 = y };
            }
            else lVector.Y2 = y;
          }
        }
      }

      return lList;
    } //


    // The text was converted to vectors.
    // Now we cut out the sequence we need for the display.
    internal List<Line> getVectorSection(ref int xSectionFrom, ref int xSectionTo, float xEnd, float xWidth) {
      int lCount = _TextAsVectorChain.Count;
      float lStart = xEnd - xWidth;

      // find the right section
      do {
        xSectionTo++;
        if (xSectionTo >= lCount) { xSectionTo = lCount - 1; break; }
        if (xEnd < _TextAsVectorChain[xSectionTo].X) break;
      } while (true);

      do {
        if (lStart < 0) break; // to allow empty spaces at the beginning of the slogan
        if (xSectionFrom >= lCount) return null;
        if (lStart < _TextAsVectorChain[xSectionFrom].X) break;
        xSectionFrom++;
      } while (true);


      // clone that section
      List<Line> lList = new List<Line>();
      for (int x = xSectionFrom; x <= xSectionTo; x++) {
        Line lClone = _TextAsVectorChain[x].Clone();
        lClone.X -= lStart; // adjust the X-axis
        lList.Add(lClone);
      }

      return lList;
    } //

  } // class
} // namespace
namespace Ticker {
  public class Line {
    public float X { get; set; }
    public float Y1 { get; set; }
    public float Y2 { get; set; }

    public Line Clone() {
      Line lLine = new Line();
      lLine.X = X;
      lLine.Y1 = Y1;
      lLine.Y2 = Y2;
      return lLine;
    } //

  } // class
} // namespace

XML (part 3, basics, advanced), serialize

We are still working with the environment of part 2. We are using the embedded file “Walmart.xml”.

In part 1 we were loading an XML file the following way:

public static void LoadXml() {
    string lDesktopPath = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory) + @"\";
    string lFile = lDesktopPath + "Walmart.xml";
 
    XDocument lXDocument = XDocument.Load(lFile);
 
    // food (using WmItem)
    WmItem[] lFood = (from lData in lXDocument.Descendants("Walmart").Descendants("food")
                        select new WmItem(
                            lData.Element("name").Value,
                            double.Parse(lData.Element("price").Value),
                            lData.Element("description").Value)
                ).ToArray();
 
    foreach (WmItem lItem in lFood) Console.WriteLine(lItem);
 
    Console.WriteLine();
 
    // electronic (quick and dirty, using var)
    var lElectronic = from lData in lXDocument.Descendants("Walmart").Descendants("electronic")
                        select lData;
 
    foreach (var lItem in lElectronic) {
        Console.WriteLine(lItem);
        Console.WriteLine();
        Console.WriteLine(lItem.Element("name").Value);
        Console.WriteLine(lItem.Element("price").Value);
        Console.WriteLine(lItem.Element("description").Value);
    }
 
    Console.ReadLine();
} //

LINQ queries were parsing the XML file and initialized the objects.
Well, there is a more direct way out there. Let the .Net framework do the dirty job. Just make use of properties. Here are some of them:

Serializable
Tells that data can be arranged in a sequence. The object can then be sent across networks or be saved.

Xml Attributes

XmlRoot
This is the root of all elements. Each XML file can only have one root element. Compared to hard disk drives on your computer it would be the “C:” directory.

XmlElement
A field or property of an object becomes an XML element during the conversion.

XmlText
A text field. Only one instance of the XmlTextAttribute can be applied in a class.

XmlAttribute
This attribute is self-explanatory. Use it whenever you want to add attributes to XML items.

Add the following two classes in your project. They define the structure of your XML file. You do not have to use attributes for all types. Attributes are optional. This means you can add as many non-Xml-Elements to a class as you like. They are simply ignored during (de-)serialization processes.

[Serializable]
[XmlRoot("Walmart", Namespace = "")]
public class WalmartWorld {
    [XmlElement("food")]
    public List<WalmartItem> Food { get; set; }

    [XmlElement("electronic")]
    public List<WalmartItem> Electronic { get; set; }

    [XmlText]
    public string Text { get; set; }
} // class

[Serializable]
[XmlRoot("Walmart", Namespace = "")]
public class WalmartItem {
    [XmlAttribute("attr")]
    public string Attribute { get; set; }

    [XmlElement("name")]
    public string Name { get; set; }

    [XmlElement("price")]
    public double Price { get; set; }

    [XmlElement("description")]
    public string Description { get; set; }

    [XmlElement("somethingElse")]
    public string SomethingElse { get; set; }

    public override string ToString() {
        return Name.PadRight(12) + 
            Price.ToString("#,##0.00").PadLeft(8) + " " + 
            Description + 
            (string.IsNullOrEmpty(SomethingElse) ? string.Empty : ("  !!! => " + SomethingElse));
    } //
} // class

Step one is to load our embedded file “Walmart.xml” using above classes. The objects will be created for you. There is no need for further parsing or instanciating.

public static WalmartWorld LoadObjects() {
    string lPath = "DemoApp.XmlFiles.Walmart.xml";
    Assembly lAssembly = Assembly.GetExecutingAssembly();

    using (Stream lStream = lAssembly.GetManifestResourceStream(lPath)) {
        XmlSerializer lXmlSerializer = new XmlSerializer(typeof(WalmartWorld));

        using (StreamReader lStreamReader = new StreamReader(lStream)) {

            return lXmlSerializer.Deserialize(lStreamReader) as WalmartWorld;
        }
    }
} //

Above method returns fully initialized classes. Nice, eh?
The code is amazingly short. Let’s convert and save objects on the desktop with this code:

public static void SaveObjects(WalmartWorld xWalmartWorld) {
    string lDesktopPath = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory) + @"\";
    string lFile = lDesktopPath + "Walmart2.xml";

    var lXmlSerializer = new XmlSerializer(typeof(WalmartWorld));

    using (var lStreamWriter = new StreamWriter(lFile)) {
        lXmlSerializer.Serialize(lStreamWriter, xWalmartWorld);
    }    
} //

Again, the code could not be shorter.
You can test above methods with the following:

public static void SerializerTest() {
            
    // load
    WalmartWorld lWalmartWorld = LoadObjects();
    if (lWalmartWorld == null) return; // error

    // print results
    foreach (WalmartItem f in lWalmartWorld.Food) Console.WriteLine(f.ToString());
    foreach (WalmartItem e in lWalmartWorld.Electronic) Console.WriteLine(e.ToString());

    // add some stuff
    lWalmartWorld.Text = "Luis Armstrong had a fabulous voice.";
    lWalmartWorld.Food[2].Attribute = "What a wonderful world.";

    // save
    SaveObjects(lWalmartWorld);

    Console.ReadLine();
} //

Have a look at your file “Walmart2.xml”, which was saved onto your desktop. It is not exactly the same as “Walmart.xml”. We added some text and attributes.
Add “Walmart2.xml” to your Visual Studio Solution Explorer and see that you can load it without any trouble. Remember to set the file property “Build Action” to “Embedded Resource”.
You can also use a file stream to load the XML file. The program just gets shorter again, not longer:

public static WalmartWorld LoadXmlAsObject() {
    string lDesktopPath = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory) + @"\";
    string lFile = lDesktopPath + "Walmart2.xml";

    XmlSerializer lXmlSerializer = new XmlSerializer(typeof(WalmartWorld));

    using (StreamReader lStreamReader = new StreamReader(lFile)) {
        return lXmlSerializer.Deserialize(lStreamReader) as WalmartWorld;
    }
} //

XML (part 2, basics), embedded, XDocument, XmlDocument, XmlTextReader, XPathNavigator

Embedded

We covered basic file IO for XML files yesterday. This is very useful for settings files. There are some XML files though that you want to hide from the user. I am not talking about encryption. Today we are going to embed the XML file the project itself.
We are still using yesterday’s “Walmart.xml” example. Create a folder “XmlFiles” in your Solution Explorer and drag the file into it.

XmlFileLocation

Now right-click the file “Walmart.xml” and select “Properties”. Change the “Build Action” to “Embedded Resource”.

EmbeddedResourceProperty

Use the following code to load the resource as an XDocument:

// since .Net 3.5
// namespace System.Xml.Linq
// xPath: namespace.folder.file.ext => here: "DemoApp.XmlFiles.Walmart.xml"
public static XDocument getAssemblyXDocument(string xPath) {
    Assembly lAssembly = Assembly.GetExecutingAssembly();
    using (Stream lStream = lAssembly.GetManifestResourceStream(xPath)) {
        XDocument lXDocument = XDocument.Load(lStream);
        return lXDocument;
    }
} //

You can also load the document as an XmlDocument.

// since .Net 1.1
// namespace System.Xml
// xPath: namespace.folder.file.ext => here: "DemoApp.XmlFiles.Walmart.xml"
public static XmlDocument getAssemblyXml(string xPath) {
    Assembly lAssembly = Assembly.GetExecutingAssembly();
    using (Stream lStream = lAssembly.GetManifestResourceStream(xPath)) {
        XmlDocument lXmlDocument = new XmlDocument();
        lXmlDocument.Load(lStream);
        return lXmlDocument;
    }
} //
public static void LoadXmlExample() {
    string lPath = "DemoApp.XmlFiles.Walmart.xml";
    XmlDocument lXml = getAssemblyXml(lPath);
    XDocument lXDoc = getAssemblyXDocument(lPath);
} //

Using an XmlDocument is similar to XDocument. XmlDocument does not support LINQ, which is a big disadvantage.

XmlDocument reads the entire document and initializes a tree structure, which needs a lot of memory. Processing bigger Xml files can cause issues. The tree can be accessed either by browsing through the nodes or by using XPath queries.
You can browse through documents forward/backward and carry out modifications.

XDocument (LINQ to XML) is easy to code and understand. Again the entire document is loaded into memory. To read Xml files we can use the XDocument or XElement class. Both classes support Load() methods. XElement represents an XML fragment while XDocument represents an entire XML document.

XmlTextReader is very fast and memory efficient. Unlike XmlDocument it can only read in forward direction. Searching for something specific is awkward. As the name says, there is no method to write or to modify data. And data validation is also left behind.

XPathNavigator is more complex and slower than XmlReader. XPathNavigator has to read enough data to be able to execute XPath queries. XmlDocument internally uses XPathNavigator. Using XPathNavigator directly is faster than using it via XmlDocument.

Using XmlTextReader:

public static void UseTextReader() {
    string lPath = "DemoApp.XmlFiles.Walmart.xml";
    Assembly lAssembly = Assembly.GetExecutingAssembly();

    using (Stream lStream = lAssembly.GetManifestResourceStream(lPath)) {
        using (XmlTextReader lXmlReader = new XmlTextReader(lStream)) {
            while (lXmlReader.Read()) {
                if (!lXmlReader.IsStartElement()) continue;
                switch (lXmlReader.Name) {
                    case "name":
                        Console.WriteLine("name: " +  lXmlReader.ReadString());
                        break;
                    case "price":
                        Console.WriteLine("price: " + lXmlReader.ReadString());
                        break;
                    case "description":
                        Console.WriteLine("description: " + lXmlReader.ReadString());
                        break;
                    case "somethingElse":
                        Console.WriteLine("somethingElse: " + lXmlReader.ReadString());
                        break;
                    default:
                        Console.WriteLine(Environment.NewLine + lXmlReader.Name);
                        break;
                }
            }
        }
    }
    Console.ReadLine();
} //

example output:

Walmart

food
name: Banana
price: 1.99
description: Mexican delicious

food
name: Rice
price: 0.79
description: the best you can get

food
name: Cornflakes
price: 3.85
description: buy some milk

food
name: Milk
price: 1.43
description: from happy cows

electronic
name: Kindle fire
price: 100
description: Amazon loves you
somethingElse: the perfect Xmas gift for your kids

food
name: baked beans
price: 1.35
description: very British

Using XPathNavigator:

public static void UseXPathNavigator() {
    string lPath = "DemoApp.XmlFiles.Walmart.xml";
    Assembly lAssembly = Assembly.GetExecutingAssembly();

    using (Stream lStream = lAssembly.GetManifestResourceStream(lPath)) {
        XPathDocument lXPathDoc = new XPathDocument(lStream);
        XPathNavigator lXNavi = lXPathDoc.CreateNavigator();
        lXNavi.MoveToRoot();
        lXNavi.MoveToFirstChild();

        do {
            if (lXNavi.MoveToFirstAttribute()) {
                Console.WriteLine(lXNavi.Name + "=" + lXNavi.Value);
                lXNavi.MoveToParent();
            }

            if (!lXNavi.MoveToFirstChild()) continue;
            do {
                if (!lXNavi.MoveToFirstChild()) break;
                do {
                    Console.WriteLine(lXNavi.Name + "=" + lXNavi.Value);
                } while (lXNavi.MoveToNext());
                Console.WriteLine();
                lXNavi.MoveToParent();
            } while (lXNavi.MoveToNext());
            lXNavi.MoveToParent();
        } while (lXNavi.MoveToNext());

        Console.ReadLine();
    }
} //

example output:
name=Banana
price=1.99
description=Mexican delicious

name=Rice
price=0.79
description=the best you can get

name=Cornflakes
price=3.85
description=buy some milk

name=Milk
price=1.43
description=from happy cows

name=Kindle fire
price=100
description=Amazon loves you
somethingElse=the perfect Xmas gift for your kids

name=baked beans
price=1.35
description=very British

Using XPathNavigator with query:

public static void UseXPathNavigator(string xQuery) {
    string lPath = "DemoApp.XmlFiles.Walmart.xml";
    Assembly lAssembly = Assembly.GetExecutingAssembly();

    using (Stream lStream = lAssembly.GetManifestResourceStream(lPath)) {
        XPathDocument lXPathDoc = new XPathDocument(lStream);
        XPathNavigator lXNavi = lXPathDoc.CreateNavigator();

        XPathNodeIterator lIterator = lXNavi.Select(xQuery);

        if (lIterator.Count <= 0) {
            Console.WriteLine("Nothing found!");
            return;
        }

        while (lIterator.MoveNext()) {
            Console.WriteLine(lIterator.Current.Value);
        }
    }            
} //

public static void TestQueries() {
    UseXPathNavigator(@"/Walmart/electronic");
    Console.WriteLine();
    UseXPathNavigator(@"/Walmart/food[name='Banana']");
            
    Console.ReadLine();
} //

example output:
Kindle fire100Amazon loves youthe perfect Xmas gift for your kids

Banana1.99Mexican delicious

My opinion is: Make use of LINQ to XML. It is very comfortable and can save you a lot of time. If you have a lot of data, then you should consider a database rather than XML files.

We will discuss XML object serialization tomorrow. Let C# save and load objects the nice way.