Category Archives: DataBinding
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
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
WPF Charts (Part 2)
I have been quite busy in the last weeks, I will resume blogging on a regular basis again.
Today, we are enhancing the previous chart Part1 with some new features:
- One curve has circles around all points. These become transparent when you hover over them.
I chose a pretty large size to make them more obvious. - The shared X-Axis text labels are rotated by 90 degrees. Feel free to test other angles like 45 degrees.
- A three second interval timer appends new points to the chart.
- A text field shows the object types while the mouse hovers over the chart elements. This is the entry point to examine chart objects, find positions and generate on the fly ToolTips.
Some code was commented out. Play with these code pieces. You can change the following behavior:
- Hide the legend by setting its width to zero.
- Add a non-shard Axis.
- Change the color to blue rather transparent when hovering over a point.
- Besides the above, play with the code as much as you like – especially the XAML part.
In this example I used the .NET ReadOnlyObservableCollection. This collection is a wrapper around the well known ObservableCollection. The readonly collection cannot be changed (if there was any chart element to eg. cut a point out). But you can access the wrapped read/write ObservableCollection. This is a nice approach. For the outer world you have an encapsulated object. From behind the curtain you still have full access. The chart automatically updates when a new point is added to the read/write ObservableCollection.
Once again the code is pretty much self explanatory. So I am not adding a long post text.
<Window x:Class="Demo.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:Demo" xmlns:datavisualization="clr-namespace:System.Windows.Controls.DataVisualization;assembly=System.Windows.Controls.DataVisualization.Toolkit" xmlns:chart="clr-namespace:System.Windows.Controls.DataVisualization.Charting;assembly=System.Windows.Controls.DataVisualization.Toolkit" Title="Demo Window" Loaded ="Window_Loaded"> <Grid> <TextBox Name="InfoBox" Text="" Height="Auto"/> <chart:Chart Name="myChart" Title="2014" Width="Auto" Height="Auto" MinWidth="400" MinHeight="300" MouseMove="OnMouseMove"> <!-- hide the legend --> <!--<chart:Chart.LegendStyle> <Style TargetType="datavisualization:Legend"> <Setter Property="Width" Value="0" /> </Style> </chart:Chart.LegendStyle>--> <chart:LineSeries Title="Volkswagen" ItemsSource="{Binding Points}" IndependentValueBinding="{Binding Date}" DependentValueBinding="{Binding PriceVW}" MouseMove="OnMouseMove"> <chart:LineSeries.DependentRangeAxis> <chart:LinearAxis Orientation="Y" Title="Y-Axis Volkswagen" ShowGridLines="True" /> </chart:LineSeries.DependentRangeAxis> <chart:LineSeries.DataPointStyle> <Style TargetType="{x:Type chart:LineDataPoint}"> <Setter Property="Background" Value="Red" /> <Setter Property="Width" Value="20" /> <Setter Property="Height" Value="20" /> <Style.Triggers> <Trigger Property="IsMouseOver" Value="True"> <!--<Setter Property="Background" Value="Blue"/>--> <Setter Property="Background" Value="Transparent" /> </Trigger> </Style.Triggers> </Style> </chart:LineSeries.DataPointStyle> </chart:LineSeries> <chart:LineSeries Title="Daimler" ItemsSource="{Binding Points}" IndependentValueBinding="{Binding Date}" DependentValueBinding="{Binding PriceDaimler}"> <chart:LineSeries.DependentRangeAxis> <chart:LinearAxis Orientation="Y" Title="Y-Axis Daimler" /> </chart:LineSeries.DependentRangeAxis> <!--<chart:LineSeries.IndependentAxis > <chart:DateTimeAxis Orientation="X" Title="non-shared axis" /> </chart:LineSeries.IndependentAxis>--> <chart:LineSeries.DataPointStyle> <Style TargetType="{x:Type chart:LineDataPoint}"> <Setter Property="Background" Value="Green"/> <Setter Property="Height" Value="0"/> <Setter Property="Width" Value="0"/> </Style> </chart:LineSeries.DataPointStyle> </chart:LineSeries> <chart:Chart.Axes> <chart:DateTimeAxis Name="SharedXAxis" Orientation="X" Title="shared X-Axis" ShowGridLines="True"> <!--rotate the X-Axis labels --> <chart:DateTimeAxis.AxisLabelStyle> <Style TargetType="chart:DateTimeAxisLabel"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="chart:DateTimeAxisLabel"> <TextBlock Text="{TemplateBinding FormattedContent}"> <TextBlock.LayoutTransform> <RotateTransform Angle="90" CenterX = "40" CenterY = "30"/> </TextBlock.LayoutTransform> </TextBlock> </ControlTemplate> </Setter.Value> </Setter> </Style> </chart:DateTimeAxis.AxisLabelStyle> </chart:DateTimeAxis> </chart:Chart.Axes> </chart:Chart> </Grid> </Window>
using System.Windows; using System.Windows.Controls.DataVisualization.Charting; using System.Windows.Input; namespace Demo { public partial class MainWindow : Window { private Model _Model; public MainWindow() { InitializeComponent(); } // constructor private void Window_Loaded(object sender, RoutedEventArgs e) { ViewModel lViewModel = new ViewModel(myChart); DataContext = lViewModel; _Model = new Model(lViewModel); } // private void OnMouseMove(object sender, MouseEventArgs e) { IInputElement lInputElement = sender as IInputElement; // == Chart, LineSeries ... Chart lChart = sender as Chart; LineSeries lLineSeries = sender as LineSeries; Point lPoint = e.GetPosition(lInputElement); if (lChart != null) { IInputElement lSelection = lChart.InputHitTest(lPoint); if (lSelection == null) return; InfoBox.Text = lSelection.GetType().ToString(); } else if (lLineSeries != null) { IInputElement lSelection = lLineSeries.InputHitTest(lPoint); if (lSelection == null) return; InfoBox.Text = lSelection.GetType().ToString(); } } // } // class } // namespace
using System; using System.Linq; using System.Windows.Threading; namespace Demo { public class Model { private ViewModel _ViewModel; public Model(ViewModel xViewModel) { _ViewModel = xViewModel; DispatcherTimer lTimer = new DispatcherTimer(); lTimer.Interval = new TimeSpan(0, 0, 3); lTimer.Tick += new EventHandler(Timer_Tick); lTimer.Start(); } // constructor void Timer_Tick(object sender, EventArgs e) { Random r = new Random(); PriceCluster lPriceCluster = _ViewModel.Points.Last(); double lVW = lPriceCluster.PriceVW * (1 + ((2.0 * (r.NextDouble() - 0.5)) / 30.0)); double lDaimler = lPriceCluster.PriceDaimler * (1 + ((2.0 * (r.NextDouble() - 0.5)) / 30.0)); _ViewModel.AddPoint(lPriceCluster.Date.AddDays(1), lVW, lDaimler); } // } // class } // namespace
using System; using System.Collections.ObjectModel; using System.Windows.Controls.DataVisualization.Charting; namespace Demo { public class ViewModel { private readonly Chart _Chart; public ReadOnlyObservableCollection<PriceCluster> Points { get; private set; } private ObservableCollection<PriceCluster> _Points = new ObservableCollection<PriceCluster>(); public ViewModel(Chart xChart) { _Chart = xChart; AddPoint(new DateTime(2014, 04, 10), 67.29, 13.85); AddPoint(new DateTime(2014, 04, 11), 66.15, 13.66); AddPoint(new DateTime(2014, 04, 14), 66.22, 13.67); AddPoint(new DateTime(2014, 04, 15), 63.99, 13.49); AddPoint(new DateTime(2014, 04, 16), 65.32, 13.62); AddPoint(new DateTime(2014, 04, 17), 67.29, 13.73); AddPoint(new DateTime(2014, 04, 22), 68.72, 13.91); AddPoint(new DateTime(2014, 04, 23), 67.85, 13.84); AddPoint(new DateTime(2014, 04, 24), 67.75, 13.78); AddPoint(new DateTime(2014, 04, 25), 66.29, 13.60); AddPoint(new DateTime(2014, 04, 28), 66.99, 13.73); AddPoint(new DateTime(2014, 04, 29), 67.79, 13.91); AddPoint(new DateTime(2014, 04, 30), 66.73, 13.79); AddPoint(new DateTime(2014, 05, 02), 66.24, 13.10); AddPoint(new DateTime(2014, 05, 05), 65.90, 13.08); AddPoint(new DateTime(2014, 05, 06), 65.16, 13.04); AddPoint(new DateTime(2014, 05, 07), 64.80, 13.18); AddPoint(new DateTime(2014, 05, 08), 65.00, 13.45); AddPoint(new DateTime(2014, 05, 09), 64.52, 13.42); AddPoint(new DateTime(2014, 05, 12), 65.28, 13.58); AddPoint(new DateTime(2014, 05, 13), 66.48, 13.40); AddPoint(new DateTime(2014, 05, 14), 66.74, 13.26); AddPoint(new DateTime(2014, 05, 15), 66.00, 12.97); AddPoint(new DateTime(2014, 05, 16), 65.21, 13.08); AddPoint(new DateTime(2014, 05, 19), 66.02, 13.38); AddPoint(new DateTime(2014, 05, 20), 66.46, 13.42); AddPoint(new DateTime(2014, 05, 21), 67.15, 13.84); AddPoint(new DateTime(2014, 05, 22), 67.52, 13.84); AddPoint(new DateTime(2014, 05, 23), 68.14, 14.06); AddPoint(new DateTime(2014, 05, 26), 69.61, 14.17); AddPoint(new DateTime(2014, 05, 27), 69.56, 14.15); AddPoint(new DateTime(2014, 05, 28), 69.29, 14.17); AddPoint(new DateTime(2014, 05, 29), 69.65, 14.18); AddPoint(new DateTime(2014, 05, 30), 69.70, 14.29); AddPoint(new DateTime(2014, 06, 02), 69.32, 14.31); AddPoint(new DateTime(2014, 06, 03), 69.68, 14.32); AddPoint(new DateTime(2014, 06, 04), 69.31, 14.31); AddPoint(new DateTime(2014, 06, 05), 70.31, 14.34); AddPoint(new DateTime(2014, 06, 06), 70.24, 14.42); AddPoint(new DateTime(2014, 06, 09), 70.09, 14.42); AddPoint(new DateTime(2014, 06, 10), 70.08, 14.47); AddPoint(new DateTime(2014, 06, 11), 69.66, 14.30); AddPoint(new DateTime(2014, 06, 12), 69.49, 14.26); AddPoint(new DateTime(2014, 06, 13), 69.12, 14.42); AddPoint(new DateTime(2014, 06, 16), 69.05, 14.44); AddPoint(new DateTime(2014, 06, 17), 69.65, 14.43); AddPoint(new DateTime(2014, 06, 18), 69.62, 14.62); AddPoint(new DateTime(2014, 06, 19), 70.10, 14.93); AddPoint(new DateTime(2014, 06, 20), 70.08, 14.93); AddPoint(new DateTime(2014, 06, 23), 69.46, 14.97); AddPoint(new DateTime(2014, 06, 24), 69.04, 15.06); AddPoint(new DateTime(2014, 06, 25), 68.71, 14.89); AddPoint(new DateTime(2014, 06, 26), 68.14, 15.12); AddPoint(new DateTime(2014, 06, 27), 68.33, 15.17); AddPoint(new DateTime(2014, 06, 30), 68.40, 15.08); AddPoint(new DateTime(2014, 07, 01), 69.19, 15.21); AddPoint(new DateTime(2014, 07, 02), 69.72, 15.20); AddPoint(new DateTime(2014, 07, 03), 70.44, 15.31); AddPoint(new DateTime(2014, 07, 04), 70.44, 15.16); AddPoint(new DateTime(2014, 07, 07), 69.28, 14.95); AddPoint(new DateTime(2014, 07, 08), 68.15, 14.84); AddPoint(new DateTime(2014, 07, 09), 68.16, 14.73); AddPoint(new DateTime(2014, 07, 10), 67.05, 14.43); AddPoint(new DateTime(2014, 07, 11), 66.68, 14.50); AddPoint(new DateTime(2014, 07, 14), 67.61, 14.60); AddPoint(new DateTime(2014, 07, 15), 67.28, 14.70); AddPoint(new DateTime(2014, 07, 16), 67.77, 14.89); AddPoint(new DateTime(2014, 07, 17), 66.56, 14.53); AddPoint(new DateTime(2014, 07, 18), 65.40, 14.52); AddPoint(new DateTime(2014, 07, 21), 64.84, 14.49); AddPoint(new DateTime(2014, 07, 22), 66.09, 14.83); AddPoint(new DateTime(2014, 07, 23), 65.58, 14.74); AddPoint(new DateTime(2014, 07, 24), 66.30, 14.92); AddPoint(new DateTime(2014, 07, 25), 65.15, 14.65); AddPoint(new DateTime(2014, 07, 28), 63.08, 14.61); AddPoint(new DateTime(2014, 07, 29), 63.89, 14.71); AddPoint(new DateTime(2014, 07, 30), 63.07, 14.43); AddPoint(new DateTime(2014, 07, 31), 61.88, 14.13); AddPoint(new DateTime(2014, 08, 01), 60.85, 13.60); AddPoint(new DateTime(2014, 08, 04), 61.17, 13.58); AddPoint(new DateTime(2014, 08, 05), 60.43, 13.61); AddPoint(new DateTime(2014, 08, 06), 59.82, 13.40); AddPoint(new DateTime(2014, 08, 07), 58.95, 13.16); AddPoint(new DateTime(2014, 08, 08), 59.27, 13.16); AddPoint(new DateTime(2014, 08, 11), 60.71, 13.36); AddPoint(new DateTime(2014, 08, 12), 59.85, 13.17); AddPoint(new DateTime(2014, 08, 13), 60.66, 13.80); AddPoint(new DateTime(2014, 08, 14), 61.07, 13.77); AddPoint(new DateTime(2014, 08, 15), 59.71, 13.65); AddPoint(new DateTime(2014, 08, 18), 60.99, 13.72); AddPoint(new DateTime(2014, 08, 19), 61.60, 13.72); AddPoint(new DateTime(2014, 08, 20), 61.33, 13.82); AddPoint(new DateTime(2014, 08, 21), 62.20, 13.86); AddPoint(new DateTime(2014, 08, 22), 61.65, 13.70); AddPoint(new DateTime(2014, 08, 25), 62.88, 13.88); AddPoint(new DateTime(2014, 08, 26), 63.49, 13.87); AddPoint(new DateTime(2014, 08, 27), 63.15, 13.89); AddPoint(new DateTime(2014, 08, 28), 62.16, 13.77); AddPoint(new DateTime(2014, 08, 29), 62.24, 13.83); AddPoint(new DateTime(2014, 09, 01), 61.88, 13.92); AddPoint(new DateTime(2014, 09, 02), 61.82, 13.92); AddPoint(new DateTime(2014, 09, 03), 62.90, 14.17); AddPoint(new DateTime(2014, 09, 04), 64.14, 14.34); AddPoint(new DateTime(2014, 09, 05), 65.17, 14.40); Points = new ReadOnlyObservableCollection<PriceCluster>(_Points); } // constructor // only to be called from the dispatcher thread! public void AddPoint(DateTime xDate, double xPriceVW, double xPriceDaimler) { _Points.Add(new PriceCluster(xDate, xPriceVW, xPriceDaimler)); } // } // class } // namespace
using System; namespace Demo { public class PriceCluster { public DateTime Date { get; set; } public double PriceVW { get; set; } public double PriceDaimler { get; set; } public PriceCluster(DateTime xDate, double xPriceVW, double xPriceDaimler) { Date = xDate; PriceVW = xPriceVW; PriceDaimler = xPriceDaimler; } // constructor } // class } // namespace
WPF Charts (Part 1)
I was playing around with techniques and built a short chart demo. There are many tools out there to create charts. I prefer the good old WPF Toolkit solution on codeplex, which adds the namespace ‘System.Windows.Controls.DataVisualization.Chart’ and is supported by Microsoft. You can expect high compatibility at zero costs.
Do not confuse this one with the Extended WPF Toolkit, which is free software, but also offers a commercial solution.
We are going to create various WPF charts in the coming weeks. The programming pattern series will continue at some point afterwards. What topics I choose is always closely related to my personal interests at that time. I find it hard to motivate myself otherwise.
This is a very simple example today. I added two NumericUpDown controls to add some flavor. Well, in the WPF Toolkit they are not called NumericUpDown anymore. There are corresponding DoubleUpDown/ DecimalUpDown/IntegerUpDown controls.
The lower DoubleUpDown control in this demo is linked to the upper one. And in turn the upper one is bound to a DataContext object property. This demonstrates a chain of bindings. Hence three objects are linked together holding the same value.
You can uncomment the prepared block in the XAML code. This will influence the line color and the line thickness. This template has its limits. It does not change the color of related objects. Anyway, it is a good start.
The chart has two curves. You can influence one by using any of the two DoubleUpDown controls.
The used ObservableCollection to store the curve points could be a post on its own. Basically, it is a WPF collection, which notifies WPF when you add or remove items from/to the list. But how do you update a chart, which only changes a value of a single point? The four methods to invalidate the drawing area are somewhat not showing the expected results.
You can set the DataContext to null and then set it back to your source. This is not the fastest way. But practically speaking, changing one value does not happen very often and setting the DataContext is quick and easy. Usually you only add or remove points. If you are looking for animations, they are dealt with differently in WPF. You should have a look into System.Windows.Media.Storyboard for that. In this example I chose to simply remove and add the affected point.
You don’t have to re-insert the point at the right collection position. I just did it to easily find the same point again. A simple Chart.Add() would work as well.
WPF will most likely not show the point removal on the screen. Tell me if I am wrong. I haven’t seen any impact. I guess the Dispatcher thread is blocked while you are using it on the WPF event call. A signal, which may happen right after the event finishes, will trigger the queued removal and addition in one go.
<Window x:Class="Demo.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:tool="clr-namespace:Xceed.Wpf.Toolkit;assembly=Xceed.Wpf.Toolkit" xmlns:dv="clr-namespace:System.Windows.Controls.DataVisualization.Charting;assembly=System.Windows.Controls.DataVisualization.Toolkit" xmlns:local="clr-namespace:Demo" Title="MainWindow" Height="350" Width="525" Initialized="Window_Initialized"> <Grid> <DockPanel LastChildFill="True"> <tool:DoubleUpDown DockPanel.Dock="Top" Name="UpDown1" AllowSpin="True" Minimum="0" Maximum="100.5" Increment="0.5" ClipValueToMinMax="True" DefaultValue="0" Watermark="enter a value" MouseWheelActiveOnFocus="True" MouseWheelActiveTrigger="FocusedMouseOver" FormatString="N3" ShowButtonSpinner="True" TextAlignment="Center" Value="{Binding PriceOfDay3, Mode=OneWayToSource, FallbackValue=55.5 }" /> <tool:DoubleUpDown DockPanel.Dock="Top" Name="UpDown2" AllowSpin="True" Minimum="0" Maximum="100.5" Increment="0.5" ClipValueToMinMax="True" Value="{Binding Value, ElementName=UpDown1, Mode=TwoWay}" MouseWheelActiveOnFocus="True" MouseWheelActiveTrigger="FocusedMouseOver" FormatString="N3" ShowButtonSpinner="True" TextAlignment="Center"/> <dv:Chart Name="Chart1" Title="Test Chart" > <dv:LineSeries Title="Price" ItemsSource="{Binding Points, Delay=2500, IsAsync=False}" IndependentValueBinding="{Binding Day}" DependentValueBinding="{Binding Price}" > <dv:LineSeries.DependentRangeAxis> <dv:LinearAxis Orientation="Y" Title="Price" Minimum="50" Maximum="60" Interval="2" ShowGridLines="True"/> </dv:LineSeries.DependentRangeAxis> <!--<dv:LineSeries.Template> --><!-- change the line color to green and set the thickness --><!-- <ControlTemplate TargetType="dv:LineSeries"> <Canvas x:Name="PlotArea"> <Polyline x:Name="polyline" Points="{TemplateBinding Points}" Style="{TemplateBinding PolylineStyle}" Stroke="Green" StrokeThickness="4" /> </Canvas> </ControlTemplate> </dv:LineSeries.Template>--> </dv:LineSeries> <dv:LineSeries Title="Tax" ItemsSource="{Binding Points, Delay=2500, IsAsync=False}" IndependentValueBinding="{Binding Day}" DependentValueBinding="{Binding Tax}"> <dv:LineSeries.DependentRangeAxis> <dv:LinearAxis Orientation="Y" Title="Tax" Minimum="-10" Maximum="10" Interval="2.5"/> </dv:LineSeries.DependentRangeAxis> </dv:LineSeries> <dv:Chart.Axes> <dv:LinearAxis Orientation="X" Title="X-Axis" Interval="2" ShowGridLines="True"/> </dv:Chart.Axes> </dv:Chart> </DockPanel> </Grid> </Window>
using System; using System.Collections.ObjectModel; using System.Windows; using System.Windows.Controls.DataVisualization.Charting; namespace Demo { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } public class DataPoint { public double Day { get; set; } public double Price { get; set; } public double Tax { get; set; } } // class public class ViewModel { private readonly Chart _Chart; public ObservableCollection<DataPoint> Points { get; private set; } public double PriceOfDay3 { get { lock (this) return Points[2].Price; } set { lock (this) { DataPoint p = Points[2]; p.Price = value; Points.Remove(p); Points.Insert(2, p); // same position //Points.Add(p); // append to the end } } } // public ViewModel(Chart xChart) { _Chart = xChart; Points = new ObservableCollection<DataPoint>(); Points.Add(new DataPoint() { Day = 1.0, Price = 55, Tax = 2.0 }); Points.Add(new DataPoint() { Day = 1.5, Price = 54, Tax = 1.0 }); Points.Add(new DataPoint() { Day = 2.0, Price = 58, Tax = -1.0 }); Points.Add(new DataPoint() { Day = 3.0, Price = 55.5, Tax = 0.0 }); Points.Add(new DataPoint() { Day = 4.0, Price = 53, Tax = -2.0 }); } // constructor } // class private void Window_Initialized(object sender, EventArgs e) { ViewModel lViewModel = new ViewModel(Chart1); DataContext = lViewModel; } // } // class } // namespace
Google Authenticator
As promised a while ago I am going to describe the Google Authenticator today. I do not use my own example source code this time. There is a really nice one here that I will explain. Download the source code and get some extra information from this post. This should be enough know-how to use the Google Authenticator in your own application afterwards.
What is the Google Authenticator?
The Google Authenticator is a program that allows time based passwords. It makes the online identification process much safer. You enter your Id, your password and an extra time based password. A third-party can hardly know the time based password. Even if there is a spy program on your PC that reads all your key inputs, then the hacker only has less than 30 seconds to exploit the opportunity. Otherwise he cannot use your credentials. The time component changes constantly. This is a pretty safe approach.
To steal your identity, the hacker needs to get access to the server database and decrypt your information. This is much more difficult than reading your password by using a key Trojan.
Once again our example program is using WPF.
The XAML is pretty much straight forward. Each element (TextBlock or Image) is using Binding to the underlying object. Just the Hmac TextBlock is less intuitive. The text consists of 3 TextBlocks, which in fact look like one. The green text is the second TextBlock. There is no method to write all text into one TextBlock and then color it. This would be far more complex than this 3 TextBlock solution.
label
|
property binding
|
type
|
Identity | Identity | string |
Secret | Secret | string |
QR code | QRCodeUrl | Image |
Timestamp | Timestamp | string |
Hmac | HmacPart1, HmacPart2, HmacPart3 | string |
One-time password | OneTimePassword | string |
Seconds to go | SecondsToGo | string |
The C# source code has some hyperlinks. You should follow them in case you want to learn some detailed background information. You don’t have to do this. To apply the code in your program it should be enough to study and replicate the example source code. This code is really short and I appreciate this a lot. Many programmers make things far more complicated than needed. Are they trying to show off? I don’t know.
Let’s start from the top now.
The DispatcherTimer is the corresponding class for the good old System.Windows.Forms.Timer class. We are not using WinForms anymore. The idea behind the DispatcherTimer is that the timer fires on the Dispatcher thread. This thread is the same that your window is using. Any other thread could freeze your application.
Do not use the System.Threading.Timer class. This class is neither WPF nor WinForms related. It fires on any thread. You must not access WPF elements on arbitrary threads. On the other hand you should use the System.Threading.Timer class in case you do not want to access WPF. Don’t waste precious time of the Dispatcher thread.
The DispatcherTimer fires each 500 ms (=half second) and assigns the actual value of the 30 second countdown to the property SecondsToGo.
The properties Secret and Identity are initialized with arbitrary example data.
Then the DataContext is set. This enables the data binding between the (XAML) window and the (C#) code.
public MainWindow() { InitializeComponent(); var timer = new DispatcherTimer(); timer.Interval = TimeSpan.FromMilliseconds(500); timer.Tick += (s, e) => SecondsToGo = 30 - Convert.ToInt32(GetUnixTimestamp() % 30); timer.IsEnabled = true; Secret = new byte[] { 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x21, 0xDE, 0xAD, 0xBE, 0xEF }; Identity = "user@host.com"; DataContext = this; }
You may have noticed already that the MainWindow class implements the interface INotifyPropertyChanged. This exposes a WPF event called PropertyChanged, which is used to notify clients via binding that a property value has changed.
namespace System.ComponentModel { public interface INotifyPropertyChanged { // Occurs when a property value changes. event PropertyChangedEventHandler PropertyChanged; } }
You notify a client (UI) with the property name (string) rather than a delegate.
The standard WPF pattern looks like this:
public event PropertyChangedEventHandler PropertyChanged; // INotifyPropertyChanged implementation protected void OnPropertyChanged(string xPropertyName) { PropertyChangedEventHandler h = PropertyChanged; if (h == null) return; h(this, new PropertyChangedEventArgs(xPropertyName)); } // private string _MyProperty; public string MyProperty { get{ return _MyProperty; } set { _MyProperty = value; OnPropertyChanged(“MyProperty”); } } //
And here is the source code from the GoogleAuthenticator example:
public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged(string propertyName) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } private int _secondsToGo; public int SecondsToGo { get { return _secondsToGo; } private set { _secondsToGo = value; OnPropertyChanged("SecondsToGo"); if (SecondsToGo == 30) CalculateOneTimePassword(); } }
You can find that pattern many times in today’s example source code. Be aware that all properties use the same event, which is PropertyChanged. This makes your code look neat, but on the other hand you unfortunately start working with strings instead of delegates. It is part of the MVVM concept to separate code and GUI. For sure it makes debugging much more difficult and code execution slower.
Just to highlight it again. The Hmac consists of three elements. Therefore the property looks like this:
public byte[] Hmac { get { return _hmac; } private set { _hmac = value; OnPropertyChanged("Hmac"); OnPropertyChanged("HmacPart1"); OnPropertyChanged("HmacPart2"); OnPropertyChanged("HmacPart3"); } }
What is an Hmac? To make complex interrelations easier, let’s reduce the Hmac description to “a long calculated key”, which is used to determine our time based password. Therefore to generate the key you need some kind of time input. The other component is a static password (=Secret). I added comments in the following code excerpt:
private void CalculateOneTimePassword() { // Get the number of seconds since 1/1/1970 and devide them by 30 seconds. // Thus one Timestamp unit is 30 seconds. Timestamp = Convert.ToInt64(GetUnixTimestamp() / 30); // Convert the 64 bit integer Timestamp to a byte array (8 bytes). // eg. ba d9 c7 02 00 00 00 00 // Then reverse them (=> 00 00 00 00 02 c7 d9 ba) and write the result to the byte array "data". var data = BitConverter.GetBytes(Timestamp).Reverse().ToArray(); // Generate the Hmac key from your password (byte array) and time (byte array). Hmac = new HMACSHA1(Secret).ComputeHash(data); // Bit-operation: Get the last 4 bits of the Hmac. The results are always equal to or between 0 and 15. // The offset determines the area of the Hmac that is used to generate the time based password. Offset = Hmac.Last() & 0x0F; // The Hmac is 20 bytes long. A block of 4 bytes is used for the OneTimePassword, which changes each 30 seconds. // 15 is the highest Offset. Therefore the last used byte is number 18 (first byte is zero based). // The 19th (=last) byte is the Offset. More precisely the <a href="http://en.wikipedia.org/wiki/Nibble" title="Wiki Nibble Byte" target="_blank">right nibble</a> of the 19th byte is the Offset value. // Bit masks are applied on the selected Hmac block to limit the number. The resulting bits are rotated to the left and added together. // Basically we are looking at a manual "bit to integer" conversion. // the result is then devided by 1,000,000 and only the remainder is taken. Consequently all results are less than 1,000,000. // (The bit mask 0xff is useless. I guess it was used to emphasize the technique for readability purposes. 0x7f does make sense.) OneTimePassword = ( ((Hmac[Offset + 0] & 0x7f) << 24) | ((Hmac[Offset + 1] & 0xff) << 16) | ((Hmac[Offset + 2] & 0xff) << 8) | (Hmac[Offset + 3] & 0xff)) % 1000000; }
When I looked at the program I was trying to find the bitmap for the QR code. You would expect a method somewhere to convert bytes to an image. But this is not the case.
The GetQRCodeUrl method generates a Url. Follow this Url and you will see that it opens an image in your browser. Google does the work for you.
The programmer of that example code has also added the Url to the description on how to generate such QR code Url. Well done! My advice is to have a quick look at it.
private string GetQRCodeUrl() { // https://code.google.com/p/google-authenticator/wiki/KeyUriFormat var base32Secret = Base32.Encode(Secret); return String.Format("https://www.google.com/chart?chs=200x200&chld=M|0&cht=qr&chl=otpauth://totp/{0}%3Fsecret%3D{1}", Identity, base32Secret); }
Now we have all bits and pieces to use the Google Authenticator. But how?
1) Ask for a user identification and a password. For instance user “super@man.com” and password “Strong5rThan$trong!”.
2) Convert the password to a byte array.
3) Generate the QR code.
4) The user can scan this QR code with his android cell phone. Press “Set up account” followed by “Scan a barcode” in the Google Authenticator app.
5) The new account appears and updates in the Authenticator app. The Identity and Secret were encoded in the QR code. This is why the app knows your name already.
6) From now on you can ask for the Id, the password and the time based password.
7) Make sure to have a plan B in place. The user might lose access to his Google Authenticator app. You have to provide a new login then.
Btw. I do know some people, who do not lock their tablet PC or cell phone. The Google Authenticator obviously is not for these kind of people. The problem is not behind the screen … it is in front of the screen.
WPF Commands (part 2)
Let’s start with a program that uses Cut, Copy and Paste in two TextBoxes without writing any C# code. This is not a typo. We only need XAML for this.
<Window x:Class="DemoApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="400" Width="400"> <DockPanel LastChildFill="True" > <Menu DockPanel.Dock="Top"> <MenuItem Header="_Edit"> <MenuItem Command="{x:Static ApplicationCommands.Cut}" CommandParameter="Cut it!"/> <MenuItem Command="{x:Static ApplicationCommands.Copy}" CommandParameter="Copy it!"/> <MenuItem Command="{x:Static ApplicationCommands.Paste}" CommandParameter="Paste it!"/> </MenuItem> </Menu> <ToolBarTray Background="Gray" DockPanel.Dock="Top"> <ToolBar Band="0" BandIndex="0" > <Button Command="{x:Static ApplicationCommands.Cut}" Content="Cut" /> <Button Command="{x:Static ApplicationCommands.Copy}" Content="Copy" /> <Button Command="{x:Static ApplicationCommands.Paste}" Content="Paste" /> </ToolBar> <ToolBar Band="1" BandIndex="1"> <ToolBarPanel Orientation="Vertical"> <Label Content="Dummy0" ToolBar.OverflowMode="AsNeeded" /> <Label Content="Dummy1" ToolBar.OverflowMode="AsNeeded" /> <Label Content="Dummy2" ToolBar.OverflowMode="AsNeeded" /> </ToolBarPanel> </ToolBar> </ToolBarTray> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition Width="Auto"/> <ColumnDefinition /> </Grid.ColumnDefinitions> <TextBox TextWrapping="Wrap" Width="Auto" Grid.Column="0" Grid.ColumnSpan="1" Margin="0" >It happened that a Fox caught its tail in a trap, and in struggling to release himself lost all of it but the stump. At first he was ashamed to show himself among his fellow foxes.</TextBox> <GridSplitter Grid.Column="1" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Margin="1,0,0,0" Width="3"/> <TextBox TextWrapping="Wrap" Width="Auto" Grid.Column="2" Grid.ColumnSpan="1" Margin="0" >But at last he determined to put a bolder face upon his misfortune, and summoned all the foxes to a general meeting to consider a proposal which he had to place before them.</TextBox> </Grid> </DockPanel> </Window>
I added a Toolbar with some dummy labels just to keep the learning curve going. You remove them without any risk.
What is happening here?
Some input controls handle command events on their own. Everything is built-in already. All you need to do is to provide the Buttons or MenuItems which call these commands. The elements even enable/disable themselves. We have two textboxes in the example. These commands are applied to the element that has the focus.
How can this be achieved? The element finds the window instance and then determines what element was focused previously. This only works for Toolbars and Menus UNLESS you set the CommandTarget property manually.
Let’s add standard buttons now. You cannot see any effect when you press them. The buttons are even ghosted. To solve this we assign the names TextBox1 and TextBox2 and link the Button CommandTargets to these elements.
You can now cut or copy from TextBox1 and paste it into TextBox2.
<Window x:Class="DemoApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="400" Width="400"> <DockPanel LastChildFill="True" > <Menu DockPanel.Dock="Top"> <MenuItem Header="_Edit"> <MenuItem Command="{x:Static ApplicationCommands.Cut}" CommandParameter="Cut"/> <MenuItem Command="{x:Static ApplicationCommands.Copy}" CommandParameter="Copy"/> <MenuItem Command="{x:Static ApplicationCommands.Paste}" CommandParameter="Paste"/> </MenuItem> </Menu> <ToolBarTray Background="Gray" DockPanel.Dock="Top"> <ToolBar Band="0" BandIndex="0" > <Button Command="{x:Static ApplicationCommands.Cut}" Content="Cut" /> <Button Command="{x:Static ApplicationCommands.Copy}" Content="Copy" /> <Button Command="{x:Static ApplicationCommands.Paste}" Content="Paste" /> </ToolBar> <ToolBar Band="1" BandIndex="1"> <ToolBarPanel Orientation="Vertical"> <Label Content="Dummy0" ToolBar.OverflowMode="AsNeeded" /> <Label Content="Dummy1" ToolBar.OverflowMode="AsNeeded" /> <Label Content="Dummy2" ToolBar.OverflowMode="AsNeeded" /> </ToolBarPanel> </ToolBar> </ToolBarTray> <!-- changed --> <StackPanel Orientation="Horizontal" DockPanel.Dock="Top"> <Button Command="{x:Static ApplicationCommands.Cut}" CommandTarget="{Binding ElementName=TextBox1}" Content="Cut" /> <Button Command="{x:Static ApplicationCommands.Copy}" CommandTarget="{Binding ElementName=TextBox1}" Content="Copy" /> <Button Command="{x:Static ApplicationCommands.Paste}" CommandTarget="{Binding ElementName=TextBox2}" Content="Paste" /> </StackPanel> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition Width="Auto"/> <ColumnDefinition /> </Grid.ColumnDefinitions> <!-- changed --> <TextBox Name="TextBox1" TextWrapping="Wrap" Width="Auto" Grid.Column="0" Grid.ColumnSpan="1" Margin="0" >It happened that a Fox caught its tail in a trap, and in struggling to release himself lost all of it but the stump. At first he was ashamed to show himself among his fellow foxes.</TextBox> <GridSplitter Grid.Column="1" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Margin="1,0,0,0" Width="3"/> <!-- changed --> <TextBox Name="TextBox2" TextWrapping="Wrap" Width="Auto" Grid.Column="2" Grid.ColumnSpan="1" Margin="0" >But at last he determined to put a bolder face upon his misfortune, and summoned all the foxes to a general meeting to consider a proposal which he had to place before them.</TextBox> </Grid> </DockPanel> </Window>
But hardcoding is a really bad approach. Therefore we are going to use FocusManager.IsFocusScope=”True” instead. WPF then checks the parent focus. By default, the Window class is a focus scope as are the Menu, ContextMenu, and ToolBar classes.
The following example is flawless.
<Window x:Class="DemoApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="400" Width="400"> <DockPanel LastChildFill="True" > <Menu DockPanel.Dock="Top"> <MenuItem Header="_Edit"> <MenuItem Command="{x:Static ApplicationCommands.Cut}" CommandParameter="Cut"/> <MenuItem Command="{x:Static ApplicationCommands.Copy}" CommandParameter="Copy"/> <MenuItem Command="{x:Static ApplicationCommands.Paste}" CommandParameter="Paste"/> </MenuItem> </Menu> <ToolBarTray Background="Gray" DockPanel.Dock="Top"> <ToolBar Band="0" BandIndex="0" > <Button Command="{x:Static ApplicationCommands.Cut}" Content="Cut" /> <Button Command="{x:Static ApplicationCommands.Copy}" Content="Copy" /> <Button Command="{x:Static ApplicationCommands.Paste}" Content="Paste" /> </ToolBar> <ToolBar Band="1" BandIndex="1"> <ToolBarPanel Orientation="Vertical"> <Label Content="Dummy0" ToolBar.OverflowMode="AsNeeded" /> <Label Content="Dummy1" ToolBar.OverflowMode="AsNeeded" /> <Label Content="Dummy2" ToolBar.OverflowMode="AsNeeded" /> </ToolBarPanel> </ToolBar> </ToolBarTray> <!-- changed --> <StackPanel Orientation="Horizontal" DockPanel.Dock="Top" FocusManager.IsFocusScope="True"> <Button Command="{x:Static ApplicationCommands.Cut}" Content="Cut" /> <Button Command="{x:Static ApplicationCommands.Copy}" Content="Copy" /> <Button Command="{x:Static ApplicationCommands.Paste}" Content="Paste" /> </StackPanel> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition Width="Auto"/> <ColumnDefinition /> </Grid.ColumnDefinitions> <TextBox TextWrapping="Wrap" Width="Auto" Grid.Column="0" Grid.ColumnSpan="1" Margin="0" >It happened that a Fox caught its tail in a trap, and in struggling to release himself lost all of it but the stump. At first he was ashamed to show himself among his fellow foxes.</TextBox> <GridSplitter Grid.Column="1" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Margin="1,0,0,0" Width="3"/> <TextBox TextWrapping="Wrap" Width="Auto" Grid.Column="2" Grid.ColumnSpan="1" Margin="0" >But at last he determined to put a bolder face upon his misfortune, and summoned all the foxes to a general meeting to consider a proposal which he had to place before them.</TextBox> </Grid> </DockPanel> </Window>
The IsFocusScope approach has the advantage that the same commands apply to several controls.
Custom Commands
We are going to write our own commands now. For this we need to create a class and add a property that returns a RoutedUICommand instance. This property needs to be static. And to initialize this class you also need a static constructor.
using System.Windows.Input; namespace CustomCommands { public class PlaySound { static PlaySound() { KeyGesture lShortCut = new KeyGesture(Key.P, ModifierKeys.Control, "Ctrl+p"); InputGestureCollection InputGestureCollection = new InputGestureCollection(); InputGestureCollection.Add(lShortCut); PlaySoundCommand = new RoutedUICommand("Play", "PlaySound", typeof(PlaySound), InputGestureCollection); } // static constructor public static RoutedUICommand PlaySoundCommand { get; private set; } } // class } // namespace
The MainWindow class should look like this. The method CommandBinding_PlaySound_Executed plays the system beep sound.
using System.Windows; using System.Windows.Input; namespace DemoApp { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } private void CommandBinding_PlaySound_Executed(object sender, ExecutedRoutedEventArgs e) { System.Media.SystemSounds.Beep.Play(); MessageBox.Show("Source: " + e.Source.ToString() + Environment.NewLine + "OriginalSource: " + e.OriginalSource.ToString() + Environment.NewLine + "Parameter: " + e.Parameter.ToString()); } // } // class } // namespace
Add the class in your XAML namespace. I used xmlns:c=”clr-namespace:CustomCommands” .
<Window x:Class="DemoApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:c="clr-namespace:CustomCommands" Title="MainWindow" Height="400" Width="400"> <!-- above was changed --> <Window.CommandBindings> <!-- changed --> <CommandBinding Command="c:PlaySound.PlaySoundCommand" Executed="CommandBinding_PlaySound_Executed" /> </Window.CommandBindings> <DockPanel LastChildFill="True" > <Menu DockPanel.Dock="Top"> <MenuItem Header="_Edit"> <MenuItem Command="{x:Static ApplicationCommands.Cut}" CommandParameter="Cut"/> <MenuItem Command="{x:Static ApplicationCommands.Copy}" CommandParameter="Copy"/> <MenuItem Command="{x:Static ApplicationCommands.Paste}" CommandParameter="Paste"/> </MenuItem> <MenuItem Header="_Media"> <MenuItem Command="c:PlaySound.PlaySoundCommand" CommandParameter="Play"/> </MenuItem> </Menu> <ToolBarTray Background="Gray" DockPanel.Dock="Top"> <ToolBar Band="0" BandIndex="0" > <Button Command="{x:Static ApplicationCommands.Cut}" Content="Cut" /> <Button Command="{x:Static ApplicationCommands.Copy}" Content="Copy" /> <Button Command="{x:Static ApplicationCommands.Paste}" Content="Paste" /> </ToolBar> <ToolBar Band="1" BandIndex="1"> <ToolBarPanel Orientation="Vertical"> <Label Content="Dummy0" ToolBar.OverflowMode="AsNeeded" /> <Label Content="Dummy1" ToolBar.OverflowMode="AsNeeded" /> <Label Content="Dummy2" ToolBar.OverflowMode="AsNeeded" /> </ToolBarPanel> </ToolBar> </ToolBarTray> <StackPanel Orientation="Horizontal" DockPanel.Dock="Top" FocusManager.IsFocusScope="True"> <Button Command="{x:Static ApplicationCommands.Cut}" Content="Cut" /> <Button Command="{x:Static ApplicationCommands.Copy}" Content="Copy" /> <Button Command="{x:Static ApplicationCommands.Paste}" Content="Paste" /> <!-- changed --> <Button Command="c:PlaySound.PlaySoundCommand" Content="Play" CommandParameter="What a lovely song!" /> </StackPanel> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition Width="Auto"/> <ColumnDefinition /> </Grid.ColumnDefinitions> <TextBox TextWrapping="Wrap" Width="Auto" Grid.Column="0" Grid.ColumnSpan="1" Margin="0" >It happened that a Fox caught its tail in a trap, and in struggling to release himself lost all of it but the stump. At first he was ashamed to show himself among his fellow foxes.</TextBox> <GridSplitter Grid.Column="1" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Margin="1,0,0,0" Width="3"/> <TextBox TextWrapping="Wrap" Width="Auto" Grid.Column="2" Grid.ColumnSpan="1" Margin="0" >But at last he determined to put a bolder face upon his misfortune, and summoned all the foxes to a general meeting to consider a proposal which he had to place before them.</TextBox> </Grid> </DockPanel> </Window>
There is a shortcut to calling commands. You can create an ICommand instance and provide it via a property. The downside – what did you expect? – is that you have no shortcut key or any other comfort.
Step 1: Create a class that inherits from interface ICommand.
using System; using System.Windows.Input; namespace CustomCommands { public class PlaySound2 : ICommand { object _DependencyObject; public PlaySound2(object xDependencyObject) { _DependencyObject = xDependencyObject; } // constructor public event EventHandler CanExecuteChanged { add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested -= value; } } // public bool CanExecute(object xParameter) { return (DateTime.Now.Second % 2 == 0); // timer based example } // public void Execute(object xParameter) { //_DependencyObject.DoSomething(); System.Windows.MessageBox.Show("Parameter: " + xParameter.ToString()); System.Media.SystemSounds.Beep.Play(); } // } // class } // namespace
Step 2: instantiate that class and provide it via a property. You do not need to expose the class in your MainWindow. You can use any class. Set the DataContext to your class where the property is (or use a precise path that leads to that object).
using CustomCommands; using System; using System.Windows; using System.Windows.Input; namespace DemoApp { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); SimpleExecution = new PlaySound2("dummy"); DataContext = this; } private void CommandBinding_PlaySound_Executed(object sender, ExecutedRoutedEventArgs e) { System.Media.SystemSounds.Beep.Play(); MessageBox.Show("Source: " + e.Source.ToString() + Environment.NewLine + "OriginalSource: " + e.OriginalSource.ToString() + Environment.NewLine + "Parameter: " + e.Parameter.ToString()); } // public ICommand SimpleExecution { get; private set; } } // class } // namespace
Step 3: Bind the command in XAML.
<Window x:Class="DemoApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:c="clr-namespace:CustomCommands" Title="MainWindow" Height="400" Width="400"> <Window.CommandBindings> <CommandBinding Command="c:PlaySound.PlaySoundCommand" Executed="CommandBinding_PlaySound_Executed" /> </Window.CommandBindings> <DockPanel LastChildFill="True" > <Menu DockPanel.Dock="Top"> <MenuItem Header="_Edit"> <MenuItem Command="{x:Static ApplicationCommands.Cut}" CommandParameter="Cut"/> <MenuItem Command="{x:Static ApplicationCommands.Copy}" CommandParameter="Copy"/> <MenuItem Command="{x:Static ApplicationCommands.Paste}" CommandParameter="Paste"/> </MenuItem> <MenuItem Header="_Media"> <MenuItem Command="c:PlaySound.PlaySoundCommand" CommandParameter="Play"/> </MenuItem> </Menu> <ToolBarTray Background="Gray" DockPanel.Dock="Top"> <ToolBar Band="0" BandIndex="0" > <Button Command="{x:Static ApplicationCommands.Cut}" Content="Cut" /> <Button Command="{x:Static ApplicationCommands.Copy}" Content="Copy" /> <Button Command="{x:Static ApplicationCommands.Paste}" Content="Paste" /> </ToolBar> <ToolBar Band="1" BandIndex="1"> <ToolBarPanel Orientation="Vertical"> <Label Content="Dummy0" ToolBar.OverflowMode="AsNeeded" /> <Label Content="Dummy1" ToolBar.OverflowMode="AsNeeded" /> <Label Content="Dummy2" ToolBar.OverflowMode="AsNeeded" /> </ToolBarPanel> </ToolBar> </ToolBarTray> <StackPanel Orientation="Horizontal" DockPanel.Dock="Top" FocusManager.IsFocusScope="True"> <Button Command="{x:Static ApplicationCommands.Cut}" Content="Cut" /> <Button Command="{x:Static ApplicationCommands.Copy}" Content="Copy" /> <Button Command="{x:Static ApplicationCommands.Paste}" Content="Paste" /> <Button Command="c:PlaySound.PlaySoundCommand" Content="Play" CommandParameter="What a lovely song!" /> <!-- changed --> <Button Command="{Binding SimpleExecution}" Content="StraightForward" CommandParameter="The German way!" /> </StackPanel> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition Width="Auto"/> <ColumnDefinition /> </Grid.ColumnDefinitions> <TextBox TextWrapping="Wrap" Width="Auto" Grid.Column="0" Grid.ColumnSpan="1" Margin="0" >It happened that a Fox caught its tail in a trap, and in struggling to release himself lost all of it but the stump. At first he was ashamed to show himself among his fellow foxes.</TextBox> <GridSplitter Grid.Column="1" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Margin="1,0,0,0" Width="3"/> <TextBox TextWrapping="Wrap" Width="Auto" Grid.Column="2" Grid.ColumnSpan="1" Margin="0" >But at last he determined to put a bolder face upon his misfortune, and summoned all the foxes to a general meeting to consider a proposal which he had to place before them.</TextBox> </Grid> </DockPanel> </Window>
That’s it for today 🙂