Category Archives: Advanced
JSON to LINQ to JSON
It is about 9:30pm and I am sitting on the train home. It has been quite tough organizing my life in the last months. This is not the promised follow-up on OpenGL. There has been no time to read any book.
Anyway, this post shows how to run LINQ queries on JSON. The use can be quite broad. Imagine a shell command executing your LINQ on any JSON file. You can query into depth and build complex return tree structures with 2 or more levels. LINQ itself is highly flexible. And – just to give it the right flair – it returns results in the good old JSON format.
You could even go a bit further and insert more than just a LINQ query. In theory, you could add any C# code at run-time. The changes to this program would be minuscule.
I have sufficiently commented the code. There is a minimum overhead to get the job done.
How it works:
- The JSON data is imported using the JavaScriptSerializer, which is part of the System.Web.Extensions library reference. We try to force the result into a Dictionary<string, object>. Do not lazily use ‘object’. Some type information would get lost. I played with this and got eg. arrays instead of ArrayLists.
- The resulting structure is then used to build classes. These are converted to legible C# source code.
- The LINQ command is inserted. You can see the result in the TextBox titled ‘C# source code output’.
- A compiler instance is created and some library references are added. We compile the generated source code at run-time and then execute the binary.
- A standard JSON object is returned, rudimentary formatted and displayed as the final result.
Btw. I have inserted a horizontal and a vertical GridSplitter into the WPF XAML. You can easily change the size of the TextBoxes while playing with this tool. A few JSON examples were also added.
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" x:Class="LinqJson.MainWindow" Title="JSON LINQ JSON" Height="Auto" Width="876" d:DesignHeight="695"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="1*"/> <RowDefinition Height="43*"/> <RowDefinition Height="30"/> <RowDefinition Height="150*"/> <RowDefinition Height="30"/> <RowDefinition Height="150*"/> <RowDefinition Height="10*"/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="10*"/> <ColumnDefinition Width="250*"/> <ColumnDefinition Width="10"/> <ColumnDefinition Width="250*"/> <ColumnDefinition Width="10*"/> <ColumnDefinition/> </Grid.ColumnDefinitions> <Button Content="convert" Width="50" Height="30" Click="Button_Click" Grid.Row="1" HorizontalAlignment="Left" Grid.Column="1" Margin="0,8,0,7"/> <StackPanel Grid.Column="1" Grid.Row="2" VerticalAlignment="Bottom" HorizontalAlignment="Left" Orientation="Horizontal" Height="27" > <Label Content="JSON input" VerticalAlignment="Bottom" HorizontalAlignment="Left" Margin="0,1" /> <ComboBox Width="240" Margin="50,3,3,3" SelectionChanged="ComboBox_SelectionChanged" VerticalAlignment="Bottom"> <ComboBoxItem Name="ex1">Example 1</ComboBoxItem> <ComboBoxItem Name="ex2">Example 2</ComboBoxItem> <ComboBoxItem Name="ex3">Example 3</ComboBoxItem> <ComboBoxItem Name="ex4">Example 4</ComboBoxItem> </ComboBox> </StackPanel> <TextBox x:Name="JsonIn" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" Grid.Column="1" Grid.Row="3"/> <GridSplitter Grid.Column="2" Grid.RowSpan="999" HorizontalAlignment="Stretch" Width="10" Background="Transparent" ResizeBehavior="PreviousAndNext"/> <GridSplitter Grid.Row="4" Grid.ColumnSpan="999" VerticalAlignment="Top" HorizontalAlignment="Stretch" Height="10" Background="Transparent" ResizeBehavior="PreviousAndNext" /> <Label Content="JSON output" Grid.Column="3" Grid.Row="2" VerticalAlignment="Bottom" /> <TextBox x:Name="JsonOut" VerticalAlignment="Stretch" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" Grid.Column="3" Grid.Row="3" /> <Label Content="C# source code output" Grid.Column="3" Grid.Row="4" VerticalAlignment="Bottom" /> <TextBox x:Name="CSharpSourceCode" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" Grid.Column="3" Grid.Row="5" /> <Label Content="LINQ input" Grid.Column="1" Grid.Row="4" VerticalAlignment="Bottom"/> <TextBox x:Name="LinqIn" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto" Grid.Column="1" Grid.Row="5" /> </Grid> </Window>
using Microsoft.CSharp; using System; using System.CodeDom.Compiler; using System.Collections.Generic; using System.Reflection; using System.Text; using System.Windows; using System.Windows.Controls; namespace LinqJson { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } // constructor private void Button_Click(object sender, RoutedEventArgs e) { // -------------------------------------------------- // get the JSON input // -------------------------------------------------- string lJsonIn = JsonIn.Text; // -------------------------------------------------- // construct the C# source code (class hierarchy) // -------------------------------------------------- Converter lConverter = new Converter(); Dictionary<string, object> lTree = lConverter.JsonToDictionary(lJsonIn); lConverter.DictionariesToClasses("root", 0, lTree); var lStringBuilder = new StringBuilder(); lConverter.BuildClasses(lStringBuilder); string lCSharpSourceCode = lConverter.GetUsings() + lStringBuilder.ToString(); // -------------------------------------------------- // add the LINQ command to the source code // -------------------------------------------------- string lLinq = LinqIn.Text; string lEntryPoint = "\n\n"; lEntryPoint += "public class baseClass {\n"; lEntryPoint += " public static object executeLinq(string xJson) {\n"; lEntryPoint += " xJson = xJson.Trim();"; lEntryPoint += " if (xJson[0] == '[') xJson = \"{home: \" + xJson + \"}\";"; lEntryPoint += " var lSerializer = new JavaScriptSerializer();\n"; lEntryPoint += " var root = lSerializer.Deserialize<Class_root>(xJson);\n"; lEntryPoint += " var lResult = " + lLinq.Replace("\n", "\n ") + ";\n"; lEntryPoint += " return lSerializer.Serialize(lResult);\n"; lEntryPoint += " }\n"; lEntryPoint += "}\n"; lCSharpSourceCode += lEntryPoint; // -------------------------------------------------- // display the source code // -------------------------------------------------- CSharpSourceCode.Text = lCSharpSourceCode; // -------------------------------------------------- // compile the source code // -------------------------------------------------- var lProviderOptions = new Dictionary<string, string>(); lProviderOptions.Add("CompilerVersion", "v4.0"); var lCSharpCodeProvider = new CSharpCodeProvider(lProviderOptions); var lCompilerParameters = new CompilerParameters(); lCompilerParameters.ReferencedAssemblies.Add("System.dll"); lCompilerParameters.ReferencedAssemblies.Add("System.Core.dll"); lCompilerParameters.ReferencedAssemblies.Add("System.Data.Linq.dll"); lCompilerParameters.ReferencedAssemblies.Add("System.Threading.dll"); lCompilerParameters.ReferencedAssemblies.Add("System.Web.Extensions.dll"); lCompilerParameters.ReferencedAssemblies.Add("System.Xml.Linq.dll"); lCompilerParameters.GenerateInMemory = true; lCompilerParameters.GenerateExecutable = false; // not required, we don't have a Main() method lCompilerParameters.IncludeDebugInformation = true; var lCompilerResults = lCSharpCodeProvider.CompileAssemblyFromSource(lCompilerParameters, lCSharpSourceCode); if (lCompilerResults.Errors.HasErrors) { var lError = new StringBuilder(); foreach (CompilerError lCompilerError in lCompilerResults.Errors) { lError.AppendLine(lCompilerError.ErrorNumber + " => " + lCompilerError.ErrorText + Environment.NewLine); } JsonOut.TextWrapping = TextWrapping.Wrap; JsonOut.Text = lError.ToString(); return; } JsonOut.TextWrapping = TextWrapping.NoWrap; // -------------------------------------------------- // execute the compiled code // -------------------------------------------------- Assembly lAssembly = lCompilerResults.CompiledAssembly; Type lProgram = lAssembly.GetType("baseClass"); MethodInfo lMethod = lProgram.GetMethod("executeLinq"); object lQueryResult = lMethod.Invoke(null, new object[] { lJsonIn }); // returns a JSON string object // -------------------------------------------------- // rudimentary JSON output formatting // -------------------------------------------------- string lJsonOut = lQueryResult.ToString(); lJsonOut = lJsonOut.Replace(",", ",\n"); lJsonOut = lJsonOut.Replace(",{", ",{\n"); lJsonOut = lJsonOut.Replace("]", "]\n"); JsonOut.Text = lJsonOut; } // private void ComboBox_SelectionChanged(object xSender, SelectionChangedEventArgs e) { var lComboBox = xSender as ComboBox; switch (lComboBox.SelectedIndex) { case 0: JsonIn.Text = "{\n \"number\": 108.541,\n \"datetime\": \"1975-03-13T10:30:00\" ,\n \"serialnumber\": \"SN1234\",\n \"more\": {\n \"field1\": 123,\n \"field2\": \"hello\"\n },\n \"array\": [\n {\"x\": 2.0},\n {\"x\": 3.0},\n {\"x\": 4.0}\n ]\n}"; LinqIn.Text = "from a in root.array\nwhere a.x > 2.0M\nselect a"; break; case 1: JsonIn.Text = "[ 1, 9, 5, 7, 1, 4 ]"; LinqIn.Text = "from a in root.home\nwhere ((a == 4) || (a == 1))\nselect a"; break; case 2: JsonIn.Text = "{myLuckyNumbers: [ 1, 9, 5, 26, 7, 1, 4 ]}"; LinqIn.Text = "from x in root.myLuckyNumbers\nwhere x % 2 == 0\nselect new { simple=x, square=x*x, text=\"Field\"+x.ToString(), classInClass=new {veryOdd=x/7.0, lol=\":D\"}}"; break; case 3: string s; s = "{\"mainMenu\": {\n"; s += " \"info\": \"Great tool.\",\n"; s += " \"value\": 100.00,\n"; s += " \"menu\": {\n"; s += " \"subMenu\": [\n"; s += " {\"text\": \"New\", \"onclick\": \"NewObject()\"},\n"; s += " {\"text\": \"Open\", \"onclick\": \"Load()\"},\n"; s += " {\"text\": \"Close\", \"onclick\": \"ByeBye()\"},\n"; s += " {\"text\": \"NNN\", \"onclick\": \"Useless()\"}\n"; s += " ]}\n"; s += "}}\n"; JsonIn.Text = s; LinqIn.Text = "root.mainMenu.menu.subMenu.Where(t => t.text.StartsWith(\"N\")).Last();"; break; default: break; } } // } // class } // namespace
using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web.Script.Serialization; // requires reference to System.Web.Extensions, used by the JavaScriptSerializer namespace LinqJson { public class Converter { private Dictionary<string, object> _Classes = new Dictionary<string, object>(); public string GetUsings() { string s; s = "using System;\n"; s += "using System.Collections.Generic;\n"; s += "using System.Linq;\nusing System.Text;\n"; s += "using System.Threading.Tasks;\n"; s += "using System.Collections;\n"; s += "using System.Web.Script.Serialization;\n\n"; return s; } // public Dictionary<string, object> JsonToDictionary(string xJson) { xJson = xJson.Trim(); if (xJson[0] == '[') xJson = "{home: " + xJson + "}"; // nameless arrays cannot be converted to dictionaries var lJavaScriptSerializer = new JavaScriptSerializer(); try { return lJavaScriptSerializer.Deserialize<Dictionary<string, object>>(xJson); } catch (Exception) { return null; } } // public void BuildClasses(StringBuilder xStringBuilder) { foreach (var lClass in _Classes) { Dictionary<string, object> lMembers = lClass.Value as Dictionary<string, object>; if (lMembers == null) continue; if (lMembers.Count <= 0) continue; xStringBuilder.Append("public class Class_"); xStringBuilder.Append(lClass.Key); xStringBuilder.AppendLine(" {"); foreach (var lMember in lMembers) { object lValue = lMember.Value; string lKey = lMember.Key; Type lType = (lValue == null) ? typeof(object) : lMember.Value.GetType(); xStringBuilder.Append(new String(' ', 2)); xStringBuilder.Append("public "); if (lType.IsValueType || (lValue is string)) { xStringBuilder.Append(lType.Name); xStringBuilder.Append(" "); xStringBuilder.Append(lKey); xStringBuilder.AppendLine(";"); } else if (lValue is Dictionary<string, object>) { xStringBuilder.Append("Class_"); xStringBuilder.Append(lKey); xStringBuilder.Append(" "); xStringBuilder.Append(lKey); xStringBuilder.AppendLine(";"); } else if (lValue is ArrayList) { ArrayList lArrayList = lValue as ArrayList; var lMemberType = ArrayListType(lArrayList); // differentiate between the contents of the list if (lMemberType.IsValueType || (lMemberType.Name == "String")) { //xStringBuilder.Append(lMemberType.Name.Replace("`2")); // Dictionaries use name "Dictionary`2" xStringBuilder.Append(" List<"); xStringBuilder.Append(lMemberType.Name); xStringBuilder.Append("> "); xStringBuilder.Append(lKey); xStringBuilder.AppendLine(";"); } else { // a class xStringBuilder.Append("List<Class_" + lKey + "> "); xStringBuilder.Append(lKey); xStringBuilder.AppendLine(";"); } } } xStringBuilder.AppendLine(" }"); xStringBuilder.AppendLine(); } } // public void DictionariesToClasses(string xRootName, int xIndent, object xObject) { if (xObject == null) return; Type lType = xObject.GetType(); if (lType.IsValueType || (xObject is string)) return; var lDictionary = xObject as Dictionary<string, object>; if (lDictionary != null) { object lObj; if (!_Classes.TryGetValue(xRootName, out lObj)) { _Classes.Add(xRootName, lDictionary); } else { foreach (var lKeyValuePair in lDictionary) { // This is a weakness of the program. // Two JSON objects must not use the same name. // We would have to compare the JSON objects and determine wether to create multiple or just one C# class. _Classes[lKeyValuePair.Key] = lKeyValuePair.Value; // object type will be overridden !!!!!! } return; } foreach (var lKeyValuePair in lDictionary) { DictionariesToClasses(lKeyValuePair.Key, xIndent, lKeyValuePair.Value); } return; } var lArrayList = xObject as ArrayList; if (lArrayList != null) { object lObj; if (!_Classes.TryGetValue(xRootName, out lObj)) { lDictionary = new Dictionary<string, object>(); _Classes.Add(xRootName, lDictionary); } else lDictionary = lObj as Dictionary<string, object>; var lElementType = ArrayListType(lArrayList); if (lElementType == typeof(Dictionary<string, object>)) { var lList = lArrayList.Cast<Dictionary<string, object>>().ToList(); // upgrade our object to have stronger types foreach (var lDict in lList) { foreach (var lKeyValuePair in lDict) { lDictionary[lKeyValuePair.Key] = lKeyValuePair.Value; // object type will be overridden !!!!!! } } foreach (var lKeyValuePair in lDictionary) { DictionariesToClasses(lKeyValuePair.Key, xIndent, lKeyValuePair.Value); } } return; } } // private void Append(StringBuilder xStringbuilder, int xIndent, string xString) { xStringbuilder.Append(new String(' ', xIndent)); xStringbuilder.Append(xString); } // private void Newline(StringBuilder xStringbuilder) { xStringbuilder.AppendLine(); } // private Type ArrayListType(ArrayList xArrayList) { var lTypes = new Dictionary<string, Type>(); Type lType; string lTypeName; foreach (object o in xArrayList) { if (o == null) lType = typeof(object); else lType = o.GetType(); lTypeName = lType.Name; if (!lTypes.ContainsKey(lTypeName)) lTypes.Add(lTypeName, lType); } if (lTypes.Count == 1) return lTypes.Values.First(); // distinct return typeof(object); } // } // class } // namespace
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’.
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’.
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.
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
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
Clipboard to Text To Speech (TTS)
Since two years I am using Text To Speech (TTS). The quality has improved a lot. The spoken text can be understood and the pronunciations have reached good levels; not perfect though. Since Windows 7 there is no need to pay for a professional voice anymore. The Microsoft Windows system voices are sufficient.
First, I wanted to build my own add-on for the Firefox browser. But I quickly realised that there are too many constraints. I am using a nice tool on my Samsung Note 4 to listen to web site texts on a daily basis. That works out very well.
Nevertheless, this tool here is for home PCs.
ClipboardTTS monitors the windows clipboard and displays the current text in a WPF TextBox. In case the CheckBox “Auto” is checked the application starts speaking the text immediately. You can also generate WAV files. Adding this feature only took a few extra lines, otherwise it would not have been worth it. There are only a few use cases.
The Clipboard class offered in .Net does not provide events to monitor clipboard changes. We therefore have to use the old fashioned 32bit Windows functions. Therefore the codes section starts with imports from the windows “user32.dll”. With these you can subscribe to updates. The event WinProc notifies the application about changes. Most of these messages are disregarded in this application. We are only interested in two types of them. The first one is the Windows clipboard chain WM_CHANGECBCHAIN. You have to store the following window of the system clipboard chain, because we must forward messages to that one. This is a weird technology, but who knows what it is good for. For sure it simplifies suppressing messages without the need for any cancellation flag.
WM_DRAWCLIPBOARD is the other type we are interested in. This message tells you that the clipboard context has changed. Have a look at the C# code, you’ll quickly understand.
You could argue that the .Net Clipboard class is not needed, after all the user32.dll can do everything we need. Well, I think we should include as much .Net as possible. This is the right way to stick to the future.
Don’t forget to reference the System.Speech library in Visual Studio.
The application itself is pretty short. This is due to two facts. Windows is offering good SAPI voices and acceptable .Net support to use these voices.
http://msdn.microsoft.com/en-us/library/ee125077%28v=vs.85%29.aspx
I don’t see the point to implement an add-on for Firefox to follow the multi-platform approach. Do I have to re-invent the wheel? And check out the existing add-ons. You can hardly understand the spoken text. Some of these add-ons offer multi-language support. Yeah, but come on! You cannot understand a word. We are not in the 1990s anymore. Computers have learnt speaking very well. What is the language support good for if you cannot understand anything?
<Window x:Class="ClipboardTTS.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="ClipboardTTS" Height="140" Width="263" Topmost="True" Loaded="Window_Loaded" Closed="Window_Closed"> <DockPanel LastChildFill="True"> <DockPanel DockPanel.Dock="Top" LastChildFill="False"> <CheckBox Name="Checkbox_OnOff" DockPanel.Dock="Left" Content="Auto" Margin="5" ToolTip="Speak as soon as the clipboard text changes"/> <Button Content="Say it" DockPanel.Dock="Right" Click="Button_SayIt_Click" Width="50" Margin="5" ToolTip="Start/Stop speaking"/> <Button Name="Button_Save" Content="Save" DockPanel.Dock="Right" Click="Button_Save_Click" Width="50" Margin="5" ToolTip="Create WAV sound file"/> </DockPanel> <ComboBox Name="ComboBox_Voices" DockPanel.Dock="Top" SelectionChanged="ComboBox_Voices_SelectionChanged" ToolTip="Voice"/> <Slider Name="Slider_Volumne" DockPanel.Dock="Top" Minimum="0" Maximum="100" Value="50" ValueChanged="Slider_Volumne_ValueChanged" ToolTip="Volume" /> <TextBox Name="TextBox_Clipboard" TextChanged="TextBox_Clipboard_TextChanged" > hello world ! </TextBox> </DockPanel> </Window>
using System; using System.Collections.ObjectModel; using System.Runtime.InteropServices; using System.Speech.Synthesis; using System.Windows; using System.Windows.Interop; using System.Windows.Media; namespace ClipboardTTS { public partial class MainWindow : Window { private const int WM_DRAWCLIPBOARD = 0x0308; // change notifications private const int WM_CHANGECBCHAIN = 0x030D; // another window is removed from the clipboard viewer chain private const int WM_CLIPBOARDUPDATE = 0x031D; // clipboard changed contents [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] private static extern IntPtr SetClipboardViewer(IntPtr xHWndNewViewer); [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] private static extern bool ChangeClipboardChain(IntPtr xHWndRemove, IntPtr xHWndNewNext); [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)] private static extern IntPtr SendMessage(IntPtr xHWnd, int xMessage, IntPtr xWParam, IntPtr xLParam); private IntPtr _HWndNextViewer; // next window private HwndSource _HWndSource; // this window private string _Text = string.Empty; private SpeechSynthesizer _SpeechSynthesizer = new SpeechSynthesizer(); public MainWindow() { InitializeComponent(); } // constructor private void StartListeningToClipboard() { WindowInteropHelper lWindowInteropHelper = new WindowInteropHelper(this); _HWndSource = HwndSource.FromHwnd(lWindowInteropHelper.Handle); _HWndSource.AddHook(WinProc); _HWndNextViewer = SetClipboardViewer(_HWndSource.Handle); // set this window as a viewer } // private void StopListeningToClipboard() { ChangeClipboardChain(_HWndSource.Handle, _HWndNextViewer); // remove from cliboard viewer chain _HWndNextViewer = IntPtr.Zero; _HWndSource.RemoveHook(WinProc); } // private void SayIt(string xText) { if (string.IsNullOrWhiteSpace(xText)) return; _SpeechSynthesizer.Volume = (int)Slider_Volumne.Value; _SpeechSynthesizer.SpeakAsync(xText); } // private IntPtr WinProc(IntPtr xHwnd, int xMessageType, IntPtr xWParam, IntPtr xLParam, ref bool xHandled) { switch (xMessageType) { case WM_CHANGECBCHAIN: if (xWParam == _HWndNextViewer) _HWndNextViewer = xLParam; else if (_HWndNextViewer != IntPtr.Zero) SendMessage(_HWndNextViewer, xMessageType, xWParam, xLParam); break; case WM_DRAWCLIPBOARD: SendMessage(_HWndNextViewer, xMessageType, xWParam, xLParam); processWinProcMessage(); break; } return IntPtr.Zero; } // private void processWinProcMessage() { if (!Dispatcher.CheckAccess()) { Dispatcher.Invoke(processWinProcMessage); return; } if (!Clipboard.ContainsText()) return; string lPreviousText = _Text; _Text = Clipboard.GetText(); if (_Text.Equals(lPreviousText)) return; // do not play the same text again InsertTextIntoTextBox(_Text); if (Checkbox_OnOff.IsChecked.Value) SayIt(_Text); } // private void InsertTextIntoTextBox(string xText) { if (!TextBox_Clipboard.Dispatcher.CheckAccess()) { TextBox_Clipboard.Dispatcher.Invoke(() => InsertTextIntoTextBox(xText)); return; } TextBox_Clipboard.Text = xText; } // private void Button_SayIt_Click(object xSender, RoutedEventArgs e) { if (_SpeechSynthesizer.State == SynthesizerState.Speaking) { _SpeechSynthesizer.SpeakAsyncCancelAll(); return; } SayIt(TextBox_Clipboard.Text); } // private void TextBox_Clipboard_TextChanged(object xSender, System.Windows.Controls.TextChangedEventArgs e) { _Text = TextBox_Clipboard.Text; } // private void Window_Loaded(object xSender, RoutedEventArgs e) { ReadOnlyCollection<InstalledVoice> lVoices = _SpeechSynthesizer.GetInstalledVoices(); if (lVoices.Count < 1) return; foreach (InstalledVoice xVoice in lVoices) { ComboBox_Voices.Items.Add(xVoice.VoiceInfo.Name); } ComboBox_Voices.SelectedIndex = 0; StartListeningToClipboard(); } // private void Window_Closed(object xSender, EventArgs e) { StopListeningToClipboard(); } // private void ComboBox_Voices_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e) { string xVoice = ComboBox_Voices.SelectedItem as string; if (string.IsNullOrWhiteSpace(xVoice)) return; _SpeechSynthesizer.SelectVoice(xVoice); } // private void Slider_Volumne_ValueChanged(object xSender, RoutedPropertyChangedEventArgs<double> e) { _SpeechSynthesizer.Volume = (int)Slider_Volumne.Value; } // private Brush _OldButtonBrush = SystemColors.ControlBrush; private void Button_Save_Click(object xSender, RoutedEventArgs e) { _OldButtonBrush = Button_Save.Background; Button_Save.Background = Brushes.Salmon; Microsoft.Win32.SaveFileDialog lDialog = new Microsoft.Win32.SaveFileDialog(); string lPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); lDialog.InitialDirectory = lPath; lDialog.FileOk += FileDialog_FileOk; lDialog.Filter = "All Files|*.*|WAV (*.wav)|*.wav"; lDialog.FilterIndex = 2; lDialog.ShowDialog(); } // void FileDialog_FileOk(object xSender, System.ComponentModel.CancelEventArgs e) { Microsoft.Win32.SaveFileDialog lDialog = xSender as Microsoft.Win32.SaveFileDialog; if (lDialog == null) return; if (!Dispatcher.CheckAccess()) { Dispatcher.Invoke(() => FileDialog_FileOk(xSender, e)); return; } try { string lPathAndFile = lDialog.FileName; _SpeechSynthesizer.SetOutputToWaveFile(lPathAndFile); _SpeechSynthesizer.SpeakCompleted += SpeechSynthesizer_SpeakCompleted; SayIt(TextBox_Clipboard.Text); Button_Save.Background = _OldButtonBrush; } catch (Exception ex) { MessageBox.Show(ex.Message); } } // void SpeechSynthesizer_SpeakCompleted(object sender, SpeakCompletedEventArgs e) { _SpeechSynthesizer.SetOutputToDefaultAudioDevice(); _SpeechSynthesizer.SpeakCompleted -= SpeechSynthesizer_SpeakCompleted; } // } // class } // namespace
Timer Finetuning, Each Microsecond Counts
This is a one day excursus from my present chart posts, which will continue with my next post.
Exact timing is not an issue for most of us. Who cares if something gets executed a few millisecond earlier or later. Well, this blog was started to highlight many issues of “hard-core” programming. Thus I do care!
Back in 1985, when I was hacking on my Commodore 64, there was no such as Context Switching. To wait for a few moments you could run a loop for a number of times. And when you were running the same loop on the same C64 model again, then you had pretty much the same passed amount of time. The emphasis in on the word “same”. But these days don’t work like that. You never face the “same” situation again. Since the invention of multithreading (in fact it was called multitasking on the good old Commodore Amiga) context switching came into place.
(There are Multimedia Timers in the windows core environment. I am not going to use these. High precision inside the standard .Net framework is possible. There is no need to leave robustness behind.)
I was running several small tests to get a feeling for the .Net timing. The first and most simple approach was using PriorityBoostEnabled on the process to boost the process priority. Unfortunately its use is limited. Your priority only gets boosted, when your main window has the focus. And when your process is on a high priority anyway, it obviously won’t change a lot.
Therefore the next step was to increase the process priority to ‘ProcessPriorityClass.RealTime’. This is a dangerous setting and should only be used by skilled programmers. Your application could consume too much privileged time at the expense of the entire system, which would result in a slowed down execution time for ALL processes and services – including your process that you were originally trying to speed-up.
One important aspect is the PrivilegedProcessorTime. We’ll assume 80 milliseconds, just to have a realistic number. This would imply that the windows system was giving your process 80 ms to execute code before it switched to another process. You have to share this order of magnitude amongst all your threads. 10 ms of these are consumed by your GUI update. Make sure to execute any time sensitive code in one piece before the next context switching takes place.
As we talk about timers, it does not help you at all to be precise on one side, but then lose processor execution time to another thread or process.
Let me come up with a short story. You take the train at exactly 1 pm after waiting for 8 hours. You only have 10 minutes left to get from Austria to Belgium. That sucks, right? You obviously have to start your journey without waiting 8 hours beforehand.
The hard-core scenario would be to get changed, pack your suitcase and sleep until 12:45pm … yes, in your business suit! Then suddenly wake up, jump out of your bed, don’t kiss your wife, run to the station and use the full 8 hours for your journey. You only make it, when you arrive in Brussels before the ticket collector kicks you out, because your ticket was only valid for 8 hours.
Let’s delve into practice. In the below example you can see that getting the time-stamp does take about half a tick. When you run a long loop and try to get the time-stamp many times, then it seems to slow down. The reason for this is the context switching. Somewhere in the middle the PrivilegedProcessorTime expired. Therefore running only a few loops can show better results.
The system timer is terribly slow. Its delays and reliability are bad as bad can be. It apparently accepts a double for microseconds rather than the usual long. You would expect a higher resolution then, wouldn’t you?
Especially the first run is unpredictable. The reason for this are the CLR runtime compilation operations. Your code gets compiled shortly before the first execution. You will observe this with any timer. Start your code earlier and skip the first callback. This improves the precision for the first callback you are really waiting for.
The thread timer is more precise, but is firing too early sometimes. Its precision is above one millisecond.
The timer with the most potential isn’t really a timer. It is a thread sleep method in a loop. In my opinion that is the most advanced solution to solve the timer precision problem. The loop is running on a thread. This thread is not shared like on a task scheduler. You own that thread and nobody else will use it. You obviously should not run hundreds of threads. Run 5 of them, and you are still in the green zone. The big advantage is that you can change the priority of this thread to ‘Highest’. This gives you the attention that real geeks need. Furthermore, you won’t have any multiple code executions. There is one event running at a time. If you miss one, because the previous event was running too long, then you can still decide to not execute the next run. A general system delay would then not queue up events that you don’t want to execute anymore … as it is too late anyway. You obviously can add this feature for any timer, but this one is the safest way to easily have only one event running at a time.
Check out my MicroTimer class. It waits a little bit less than required and then calls SpinWait multiple times. Once the process thread gets processor time assigned, you most likely won’t face context switching. You wait like a disciple for the one and only messiah, running around a rigid pole.
The MicroTimer class should give you a whopping precision of 1 microsecond …. unless my usual ‘unless’ sentence states something else 😉
The example output shows the delay at the point when the timer was leaving the inner loop to raise an event. And it shows the delay at the time it recorded the data inside the event. Obviously there are some microseconds in between. Measure that time and adjust your schedule accordingly.
Computer Specs:
Lenovo Yoga 2 11”
Intel i5-4202Y 1.6 GHz processor
128GB SSD hard disk
4GB memory
Windows 8.1, 64 bit
Admittedly, the code is a bit of Spaghetti style today. Tests are really what they are. It was clear that the MicroTimer would win in the end. So my effort for a proper style went into that direction.
using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; namespace TimerPrecisionTest { class Program { private const long _NumDataPoints = 10; private static List<long> _DataPoints = new List<long>(); private static int _CurrentDataPoint = 0; private static long[] _Schedule; private static long _PeriodInMs = 3 * Stopwatch.Frequency * 1000; private static long _PeriodInTicks = 3 * Stopwatch.Frequency; private static double _FrequencyAsDouble = (double)Stopwatch.Frequency; private static AutoResetEvent _AutoResetEvent = new AutoResetEvent(false); static void Main(string[] args) { Process p = Process.GetCurrentProcess(); p.PriorityBoostEnabled = true; // every little helps p.PriorityClass = ProcessPriorityClass.Normal; Console.WriteLine("Process with normal priority:"); Console.WriteLine("Priviledged processor time for process " + p.ProcessName + " is " + p.PrivilegedProcessorTime.TotalMilliseconds.ToString("#,##0.0") + " ms"); p.PriorityClass = ProcessPriorityClass.RealTime; Console.WriteLine("Process with high priority:"); Console.WriteLine("Priviledged processor time for process " + p.ProcessName + " is " + p.PrivilegedProcessorTime.TotalMilliseconds.ToString("#,##0.0") + " ms"); Console.WriteLine("IsHighResolution system clock: " + Stopwatch.IsHighResolution); Console.WriteLine("Number of ticks per second: " + Stopwatch.Frequency.ToString("#,##0")); long a = Stopwatch.GetTimestamp(); for (int i = 0; i < 100000; i++) { long b = Stopwatch.GetTimestamp(); } long c = Stopwatch.GetTimestamp(); Console.WriteLine("Number of ticks to obtain a timestamp: " + ((c - a) / 100000.0).ToString("#,##0.00")); Console.WriteLine(); UseSystemTimer(); UseThreadingTimer(); // a simple loop Thread lThread = new Thread(new ThreadStart(UseLoop)); lThread.Priority = ThreadPriority.Highest; lThread.IsBackground = true; lThread.Start(); _AutoResetEvent.WaitOne(); testSpinWaitPrecision(); // a proper loop UseMicroTimerClass(); Console.ReadLine(); } // #region MicroTimer private static void UseMicroTimerClass() { Console.WriteLine("MICRO TIMER CLASS:"); Init(); long lMaxDelay = (3L * Stopwatch.Frequency) / 1000L; // 3 ms MicroTimer lMicroTimer = new MicroTimer(new Queue<long>(_Schedule), lMaxDelay); lMicroTimer.OnMicroTimer += OnMicroTimer; lMicroTimer.OnMicroTimerStop += OnMicroTimerStop; lMicroTimer.OnMicroTimerSkipped += OnMicroTimerSkipped; lMicroTimer.Start(); } // static void OnMicroTimerSkipped(int xSenderThreadId, long xWakeUpTimeInTicks, long xDelayInTicks) { Console.WriteLine("MicroTimer for WakeUpTime " + xWakeUpTimeInTicks + " did not run. Delay was: " + xDelayInTicks); } // static void OnMicroTimerStop(int xSenderThreadId) { Console.WriteLine("MicroTimer stopped."); PrintStats(); } // static void OnMicroTimer(int xSenderThreadId, long xWakeUpTimeInTicks, long xDelayInTicks) { RecordDatapoint(); Console.WriteLine("(Delay at wakeup time was " + xDelayInTicks.ToString("#,##0" + " tick)")); } // #endregion #region SpinWait precision private static void testSpinWaitPrecision() { Console.WriteLine(); Console.WriteLine("SpinWait tests (neglecting PrivilegedProcessorTime):"); Thread.CurrentThread.Priority = ThreadPriority.Highest; Thread.Sleep(0); // switch context at a good point long a = Stopwatch.GetTimestamp(); Thread.SpinWait(100000); long b = Stopwatch.GetTimestamp(); Console.WriteLine("Number of ticks for a SpinWait: " + (b - a).ToString("#,##0")); a = Stopwatch.GetTimestamp(); Thread.Sleep(0); for (int i = 0; i < 100; i++) { Thread.SpinWait(100000); } b = Stopwatch.GetTimestamp(); double lAverage = (b - a) / 100.0; Console.WriteLine("Average ticks for 100x SpinWaits: " + lAverage.ToString("#,##0") + " == " + (lAverage * 1000.0 * 1000.0/ Stopwatch.Frequency).ToString("#,##0.0000") + " microseconds"); // now we do get extremly precise long lEndTime = Stopwatch.GetTimestamp() + Stopwatch.Frequency; // wake up in one second Thread.Sleep(900); // imagine a timer raises an event roughly 100ms too early while (Stopwatch.GetTimestamp() < lEndTime) { Thread.SpinWait(10); // no context switching } a = Stopwatch.GetTimestamp(); Console.WriteLine("SpinWait caused an error of just: " + ((a - lEndTime) * 1000.0 * 1000.0 / _FrequencyAsDouble).ToString("#,##0.0000") + " microseconds"); Thread.CurrentThread.Priority = ThreadPriority.Normal; Console.WriteLine(); } // #endregion #region simple loop private static void UseLoop() { Thread.CurrentThread.Priority = ThreadPriority.Highest; Console.WriteLine("LOOP AND SLEEP:"); Init(); Thread.Sleep((int)getTimeMsToNextCall_Long()); while (_CurrentDataPoint < _NumDataPoints) { RecordDatapoint(); if (_CurrentDataPoint >= _NumDataPoints) break; Thread.Sleep((int)getTimeMsToNextCall_Long()); } PrintStats(); _AutoResetEvent.Set(); Thread.CurrentThread.Priority = ThreadPriority.Normal; } // #endregion #region SystemTimer private static System.Timers.Timer _SystemTimer = null; private static void UseSystemTimer() { Console.WriteLine("SYSTEM TIMER:"); Init(); _SystemTimer = new System.Timers.Timer(); _SystemTimer.AutoReset = false; _SystemTimer.Elapsed += SystemTimer_Elapsed; _SystemTimer.Interval = getTimeMsToNextCall_Double(); // do not init in constructor! _SystemTimer.Start(); _AutoResetEvent.WaitOne(); _SystemTimer.Stop(); _SystemTimer = null; PrintStats(); } // private static void SystemTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { RecordDatapoint(); // calibrate timer (we did not start with the right interval when we launched it) System.Timers.Timer lTimer = _SystemTimer; if (lTimer == null) return; if (_CurrentDataPoint >= _NumDataPoints) return; lTimer.Stop(); lTimer.Interval = getTimeMsToNextCall_Double(); lTimer.Start(); } // #endregion #region ThreadingTimer private static System.Threading.Timer _ThreadingTimer = null; private static void UseThreadingTimer() { Console.WriteLine("THREAD TIMER:"); Init(); TimerCallback lCallback = new TimerCallback(ThreadingTimer_Elapsed); _ThreadingTimer = new System.Threading.Timer(lCallback, null, getTimeMsToNextCall_Long(), (long)(Timeout.Infinite)); _AutoResetEvent.WaitOne(); _ThreadingTimer = null; PrintStats(); } // private static void ThreadingTimer_Elapsed(object xState) { RecordDatapoint(); // restart timer System.Threading.Timer lTimer = _ThreadingTimer; if (lTimer == null) return; if (_CurrentDataPoint >= _NumDataPoints) return; lTimer.Change(getTimeMsToNextCall_Long(), (long)Timeout.Infinite); } // #endregion #region miscellaneous private static void Init() { _DataPoints.Clear(); _CurrentDataPoint = 0; // init exact time schedule long lOffset = Stopwatch.GetTimestamp() + _PeriodInTicks; // we start in the future _Schedule = new long[_NumDataPoints]; for (int i = 0; i < _NumDataPoints; i++) { _Schedule[i] = lOffset; lOffset += _PeriodInTicks; } } // private static void PrintStats() { if (_DataPoints.Count < 1) return; Console.WriteLine("Average " + _DataPoints.Average()); long lMin = _DataPoints.Min(); long lMax = _DataPoints.Max(); Console.WriteLine("Min " + lMin); Console.WriteLine("Max " + lMax); Console.WriteLine("Range " + (lMax - lMin)); Console.WriteLine(); } // private static void RecordDatapoint() { long lDifference = Stopwatch.GetTimestamp() - _Schedule[_CurrentDataPoint]; // positive = late, negative = early _DataPoints.Add(lDifference); Console.WriteLine("Delay in ticks: " + lDifference.ToString("#,##0") + " == " + ((lDifference * 1000000.0) / _FrequencyAsDouble).ToString("#,##0") + " microseconds"); _CurrentDataPoint++; if (_CurrentDataPoint >= _NumDataPoints) _AutoResetEvent.Set(); } // private static long getTimeMsToNextCall_Long() { long lTicks = (_Schedule[_CurrentDataPoint] - Stopwatch.GetTimestamp()); return (1000 * lTicks) / Stopwatch.Frequency; } // private static double getTimeMsToNextCall_Double() { double lTicks = (double)(_Schedule[_CurrentDataPoint] - Stopwatch.GetTimestamp()); return (1000.0 * lTicks) / _FrequencyAsDouble; } // #endregion } // class } // namespace
using System; using System.Collections.Generic; using System.Diagnostics; using System.Threading; namespace TimerPrecisionTest { public class MicroTimer { private readonly Queue<long> _TickTimeTable; private readonly Thread _Thread; private readonly long _MaxDelayInTicks; // do not run if the delay was too long private long _NextWakeUpTickTime; public delegate void dOnMicroTimer(int xSenderThreadId, long xWakeUpTimeInTicks, long xDelayInTicks); public event dOnMicroTimer OnMicroTimer; public event dOnMicroTimer OnMicroTimerSkipped; public delegate void dQuickNote(int xSenderThreadId); public event dQuickNote OnMicroTimerStart; public event dQuickNote OnMicroTimerStop; public MicroTimer(Queue<long> xTickTimeTable, long xMaxDelayInTicks) { _TickTimeTable = xTickTimeTable; _Thread = new Thread(new ThreadStart(Loop)); _Thread.Priority = ThreadPriority.Highest; _Thread.Name = "TimerLoop"; _Thread.IsBackground = true; _MaxDelayInTicks = xMaxDelayInTicks; } // public int Start() { if ((_Thread.ThreadState & System.Threading.ThreadState.Unstarted) == 0) return -1; _Thread.Start(); return _Thread.ManagedThreadId; } // public void Stop() { _Thread.Interrupt(); } // private void Loop() { dQuickNote lOnStart = OnMicroTimerStart; if (lOnStart != null) lOnStart(_Thread.ManagedThreadId); try { while (true) { if (_TickTimeTable.Count < 1) break; _NextWakeUpTickTime = _TickTimeTable.Dequeue(); long lMilliseconds = _NextWakeUpTickTime - Stopwatch.GetTimestamp(); if (lMilliseconds < 0L) continue; lMilliseconds = (lMilliseconds * 1000) / Stopwatch.Frequency; lMilliseconds -= 50; // we want to wake up earlier and spend the last time using SpinWait Thread.Sleep((int)lMilliseconds); while (Stopwatch.GetTimestamp() < _NextWakeUpTickTime) { Thread.SpinWait(10); } long lWakeUpTimeInTicks = Stopwatch.GetTimestamp(); long lDelay = lWakeUpTimeInTicks - _NextWakeUpTickTime; if (lDelay < _MaxDelayInTicks) { dOnMicroTimer lHandler = OnMicroTimer; if (lHandler == null) continue; lHandler(_Thread.ManagedThreadId, lWakeUpTimeInTicks, lDelay); } else { dOnMicroTimer lHandler = OnMicroTimerSkipped; if (lHandler == null) continue; lHandler(_Thread.ManagedThreadId, lWakeUpTimeInTicks, lDelay); } } } catch (ThreadInterruptedException) { } catch (Exception) { Console.WriteLine("Exiting timer thread."); } dQuickNote lOnStop = OnMicroTimerStop; if (lOnStop != null) lOnStop(_Thread.ManagedThreadId); } // } // class } // namespace
Example output:
Process with normal priority:
Priviledged processor time for process TimerPrecisionTest.vshost is 109.4 ms
Process with high priority:
Priviledged processor time for process TimerPrecisionTest.vshost is 109.4 ms
IsHighResolution system clock: True
Number of ticks per second: 1,558,893
Number of ticks to obtain a timestamp: 0.27SYSTEM TIMER:
Delay in ticks: 65,625 == 42,097 microseconds
Delay in ticks: 1,414 == 907 microseconds
Delay in ticks: 1,663 == 1,067 microseconds
Delay in ticks: 1,437 == 922 microseconds
Delay in ticks: 25,829 == 16,569 microseconds
Delay in ticks: 1,532 == 983 microseconds
Delay in ticks: 14,478 == 9,287 microseconds
Delay in ticks: 14,587 == 9,357 microseconds
Delay in ticks: 14,615 == 9,375 microseconds
Delay in ticks: 14,650 == 9,398 microseconds
Average 15583
Min 1414
Max 65625
Range 64211THREAD TIMER:
Delay in ticks: 18,890 == 12,118 microseconds
Delay in ticks: 17,493 == 11,221 microseconds
Delay in ticks: 11,750 == 7,537 microseconds
Delay in ticks: 11,824 == 7,585 microseconds
Delay in ticks: 11,914 == 7,643 microseconds
Delay in ticks: 11,858 == 7,607 microseconds
Delay in ticks: 11,935 == 7,656 microseconds
Delay in ticks: 12,049 == 7,729 microseconds
Delay in ticks: 12,108 == 7,767 microseconds
Delay in ticks: 24,953 == 16,007 microseconds
Average 14477.4
Min 11750
Max 24953
Range 13203LOOP AND SLEEP:
Delay in ticks: 7,346 == 4,712 microseconds
Delay in ticks: 7,367 == 4,726 microseconds
Delay in ticks: 7,423 == 4,762 microseconds
Delay in ticks: 7,494 == 4,807 microseconds
Delay in ticks: 7,542 == 4,838 microseconds
Delay in ticks: 7,408 == 4,752 microseconds
Delay in ticks: 20,249 == 12,989 microseconds
Delay in ticks: 20,275 == 13,006 microseconds
Delay in ticks: 20,351 == 13,055 microseconds
Delay in ticks: 20,383 == 13,075 microseconds
Average 12583.8
Min 7346
Max 20383
Range 13037SpinWait tests (neglecting PrivilegedProcessorTime):
Number of ticks for a SpinWait: 4,833
Average ticks for 100x SpinWaits: 1,422 == 912.0831 microseconds
SpinWait caused an error of just: 0.0000 microsecondsMICRO TIMER CLASS:
Delay in ticks: 772 == 495 microseconds
(Delay at wakeup time was 1 tick)
Delay in ticks: 34 == 22 microseconds
(Delay at wakeup time was 1 tick)
Delay in ticks: 6 == 4 microseconds
(Delay at wakeup time was 1 tick)
Delay in ticks: 5 == 3 microseconds
(Delay at wakeup time was 1 tick)
Delay in ticks: 6 == 4 microseconds
(Delay at wakeup time was 1 tick)
Delay in ticks: 6 == 4 microseconds
(Delay at wakeup time was 1 tick)
Delay in ticks: 6 == 4 microseconds
(Delay at wakeup time was 1 tick)
Delay in ticks: 5 == 3 microseconds
(Delay at wakeup time was 1 tick)
Delay in ticks: 6 == 4 microseconds
(Delay at wakeup time was 1 tick)
Delay in ticks: 7 == 4 microseconds
(Delay at wakeup time was 2 tick)
MicroTimer stopped.
Average 85.3
Min 5
Max 772
Range 767