Blog Archives
OpenTK, entering the C# 3D world
I was amazed how short the source code can be to show complex moving objects. It does take a bit of research and the OpenTK library though. There are a lot of very good YouTube tutorials out there. Not necessarily about OpenTK, but plenty about OpenGL, which is what OpenTK is using.
I asked my daughters to draw some Minecraft style avatars with a simple icon editor. They had their fun and I got something to display in 3D. I stored three icon files (Freya.ico, Merlin.ico and Steve.ico) in a subfolder called ‘Resources’.
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
migration C#, Java, C++ (day 10), chrono, time, exceptions
I have mentioned speed quite often. Let’s measure it now!
It is remarkable that C# is dealing with exceptions faster than C++. The code execution is amazing. Anyway, when it comes to thrown exceptions (rarely happens in practice), then C++ is more than 20x faster. I knew how to build in the exceptions to see a difference. I was astonished that C++ was beyond belief compared to C# thrown exceptions. For further study follow these links:
Exceptions part 1
Exceptions part 2
Exceptions part 3
Time, Benchmark and Co.
using System; using System.Diagnostics; namespace DemoApp { public class Benchmark { void Benchmark1() { for (int i = 0; i < 1000; i++) { Console.Write("*"); } Console.WriteLine(); } // void Benchmark2() { for (int i = 0; i < 1000; i++) { try { Console.Write("*"); } catch (Exception ex) { Console.WriteLine(ex.Message); } } Console.WriteLine(); } // void Benchmark3() { for (int i = 0; i < 1000; i++) { try { Console.Write("*"); throw new Exception("OMG"); } catch (Exception ex) { } } Console.WriteLine(); } // void Benchmark4b() { Console.Write("*"); throw new Exception("OMG"); } void Benchmark4a() { for (int i = 0; i < 1000; i++) { try { Benchmark4b(); } catch (Exception ex) { } } Console.WriteLine(); } // public class StopWatch { Stopwatch _Stopwatch = new Stopwatch(); public void start() { _Stopwatch.Start(); } public void stop() { _Stopwatch.Stop(); long lElapsedTicks = _Stopwatch.ElapsedTicks; long lTicksPerSecond = Stopwatch.Frequency; double lMilliseconds = 1000.0 * (double)lElapsedTicks / (double)lTicksPerSecond; Console.WriteLine(lMilliseconds + " ms "); _Stopwatch.Start(); } }; public void test() { DateTime lNow1 = DateTime.Now; // current time DateTime lNow2 = DateTime.Now.AddSeconds(2.0); // current time plus 2 seconds double lSince1970 = DateTime.Now.Subtract(new DateTime(1970, 1, 1)).TotalSeconds; // Seconds since 1 Jan 1970 Console.WriteLine("Current local time and date: " + DateTime.Now.ToString("dd MMM yyyy, HH:mm:ss")); Console.WriteLine("1/" + Stopwatch.Frequency); // eg. 1/1,000,000,000 == 1ns // Benchmark DateTime lStart = DateTime.Now; Benchmark1(); DateTime lEnd = DateTime.Now; TimeSpan lDuration = lEnd - lStart; Console.WriteLine("Time passed ms: " + lDuration.TotalMilliseconds); // Benchmarks: let's get more precise now StopWatch lStopwatch = new StopWatch(); lStopwatch.start(); Console.Write("Benchmark1:"); Benchmark1(); lStopwatch.stop(); Console.Write("Benchmark2:"); Benchmark2(); lStopwatch.stop(); Console.Write("Benchmark3:"); Benchmark3(); lStopwatch.stop(); Console.Write("Benchmark4:"); Benchmark4a(); lStopwatch.stop(); Console.ReadKey(); } // } // class } // namespace
example output:
Current local time and date: 12 Mar 2014, 21:35:46
1/3122812
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
****************************************
Time passed ms: 62.0035
Benchmark1:*********************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
***************************************************
63.3714101265142 ms
Benchmark2:*********************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
***************************************************
133.177405492229 ms
Benchmark3:*********************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
***************************************************
6450.41872517462 ms
Benchmark4:*********************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
***************************************************
13223.9343899024 ms
#include <iostream> #include <chrono> #include <time.h> #include <string> using namespace std; using namespace std::chrono; void Benchmark1() { for (int i = 0; i < 1000; i++) { cout << "*"; } cout << endl; } // void Benchmark2() { for (int i = 0; i < 1000; i++) { try { cout << "*"; } catch (exception &ex) { cout << ex.what() << endl; } } cout << endl; } // void Benchmark3() { for (int i = 0; i < 1000; i++) { try { cout << "*"; throw exception("OMG"); } catch (exception &ex) {} } cout << endl; } // void Benchmark4b(){ cout << "*"; throw exception("OMG"); } void Benchmark4a() { for (int i = 0; i < 1000; i++) { try { Benchmark4b(); } catch (exception &ex) {} } cout << endl; } // class Stopwatch { high_resolution_clock _Clock; high_resolution_clock::time_point _From; public: void start() { _From = _Clock.now(); } void stop() { high_resolution_clock::time_point lNow = _Clock.now(); high_resolution_clock::duration lDuration = lNow - _From; intmax_t lNum = high_resolution_clock::period::num; intmax_t lDen = high_resolution_clock::period::den; double lMilliseconds = 1000.0 * lDuration.count() * (double)lNum / (double)lDen; cout << lMilliseconds << " ms " << endl; _From = _Clock.now(); } }; int main() { system_clock c1; // system clock changes will have an impact steady_clock c2; // independent from system clock changes high_resolution_clock c3; system_clock::time_point lTimePoint = system_clock::now(); cout << lTimePoint.time_since_epoch().count() << endl; lTimePoint = lTimePoint + seconds(2); cout << lTimePoint.time_since_epoch().count() << endl; time_t lRawTime; struct tm lLocalTime; const int lTimeStringLength = 50; char lTimeString[lTimeStringLength]; bool b1 = (time(&lRawTime) != -1); // Seconds since 1 Jan 1970 bool b2 = (localtime_s(&lLocalTime, &lRawTime) == 0); // Convert to local time bool b3 = (asctime_s(lTimeString, lTimeStringLength, &lLocalTime) == 0); // convert to string if (b1 && b2 && b3) cout << "Current local time and date: " << lTimeString << endl; else cerr << "Error, cannot get current time." << endl; ratio<1, 10> r1; // 1/10 ratio<2, 10> r2; // 2/10 cout << r1.num << "/" << r1.den << endl; // 1/10 cout << r2.num << "/" << r2.den << endl; // 1/5 cout << system_clock::period::num << "/" << system_clock::period::den << endl; // eg. 1/1,000,000,000 == 1ns microseconds micros(1234); // 1,234 microseconds cout << micros.count() << endl; // 1,234 nanoseconds ns = micros; // 1,234,000 nanoseconds, no loss cout << ns.count() << endl; // 1,234,000 milliseconds ms = duration_cast<milliseconds>(micros); // 1 millisecond, precision loss => duration_cast required micros += ms; cout << micros.count() << endl; // 2,234 // Benchmark steady_clock::time_point lStart = steady_clock::now(); Benchmark1(); steady_clock::time_point lEnd = steady_clock::now(); steady_clock::duration lDuration = lEnd - lStart; if (lDuration == steady_clock::duration::zero()) cout << "no time has passed" << endl; cout << duration_cast<milliseconds>(lDuration).count() << endl; // Benchmarks: let's get more precise now Stopwatch lStopwatch; lStopwatch.start(); cout << "Benchmark1:"; Benchmark1(); lStopwatch.stop(); cout << "Benchmark2:"; Benchmark2(); lStopwatch.stop(); cout << "Benchmark3:"; Benchmark3(); lStopwatch.stop(); cout << "Benchmark4:"; Benchmark4a(); lStopwatch.stop(); cin.get(); return 0; } //
example output:
13946584250282411
13946584270282411
Current local time and date: Wed Mar 12 21:07:05 20141/10
1/5
1/10000000
1234
1234000
2234
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
****************************************
80
Benchmark1:*********************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
***************************************************
86.0049 ms
Benchmark2:*********************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
***************************************************
84.0048 ms
Benchmark3:*********************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
***************************************************
311.018 ms
Benchmark4:*********************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
********************************************************************************
***************************************************
306.017 ms
package DemoApp; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.logging.Level; import java.util.logging.Logger; public class Benchmark { private void Benchmark1() { for (int i = 0; i < 1000; i++) System.out.print("*"); System.out.println(); } // private void Benchmark2() { for (int i = 0; i < 1000; i++) { try { System.out.print("*"); } catch (RuntimeException ex) { System.out.println(ex.getMessage()); } } System.out.println(); } // private void Benchmark3() { for (int i = 0; i < 1000; i++) { try { System.out.print("*"); throw new RuntimeException("OMG"); } catch (RuntimeException ex) { } } System.out.println(); } // private void Benchmark4b() { System.out.print("*"); throw new RuntimeException("OMG"); } // private void Benchmark4a() { for (int i = 0; i < 1000; i++) { try { Benchmark4b(); } catch (RuntimeException ex) { } } System.out.println(); } // public static class StopWatch { private long _Start = System.nanoTime(); private long _Stop = System.nanoTime(); public final void start() { _Start = System.nanoTime(); } public final void stop() { _Stop = System.nanoTime(); long lElapsedTicks = _Stop - _Start; double lMilliseconds = lElapsedTicks / 1000000.0; System.out.println(lMilliseconds + " ms "); _Start = System.nanoTime(); } } // class public final void test() { Calendar lNow1 = Calendar.getInstance(); // current time Calendar lNow2 = Calendar.getInstance(); lNow2.add(Calendar.SECOND, 2); // current time plus 2 seconds double lSince1970 = System.currentTimeMillis() / 1000.0; // Seconds since 1 Jan 1970 SimpleDateFormat lFormat = new SimpleDateFormat("dd MMM yyyy, HH:mm:ss"); System.out.print("Current local time and date: "); System.out.println(lFormat.format(Calendar.getInstance().getTime())); // Benchmark Calendar lStart = Calendar.getInstance(); Benchmark1(); Calendar lEnd = Calendar.getInstance(); long lDuration = lEnd.getTimeInMillis() - lStart.getTimeInMillis(); System.out.println("Time passed ms: " + lDuration); // Benchmarks: let's get more precise now StopWatch lStopwatch = new StopWatch(); lStopwatch.start(); System.out.print("Benchmark1:"); Benchmark1(); lStopwatch.stop(); System.out.print("Benchmark2:"); Benchmark2(); lStopwatch.stop(); System.out.print("Benchmark3:"); Benchmark3(); lStopwatch.stop(); System.out.print("Benchmark4:"); Benchmark4a();lStopwatch.stop(); try { System.in.read(); } catch (IOException ex) { Logger.getLogger(Benchmark.class.getName()).log(Level.SEVERE, null, ex); } } // public static void main(String[] args) { Benchmark lBenchmark = new Benchmark(); lBenchmark.test(); } // } // class
example output:
Current local time and date: 09 May 2014, 18:03:20
****************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
Time passed ms: 16
Benchmark1:****************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
18.10766 ms
Benchmark2:****************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
14.703688 ms
Benchmark3:****************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
25.99924 ms
Benchmark4:****************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
49.451426 ms
Exceptions (part 3, advanced)
For Windows Forms and WPF we have specific exceptions to deal with. Actually these are not really exceptions. They are events, which are raised whenever any unhandled exception occurs. “Unhandled” means there is nothing that catches exceptions and the entire stack did not come up with any solution. Nothing stopped the thread falling down to its lowest level, and even that level does not know what to do with the exception. You can eg. throw an exception on a button click to cause this behavior.
These events allow applications to log information about unhandled exceptions.
In Windows Forms the EventHandler is defined in System.Threading and is called ThreadExceptionEventHandler. If you do not define this event call then your application will crash when unhandled exceptions show up.
[STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.ThreadException += Application_ThreadException; Application.Run(new Form1()); } // static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e) { Exception lException = (Exception)e.Exception; MessageBox.Show(lException.Message); } //
The approach in WPF is similar. We are talking in Domains. Therefore we subscribe to AppDomain.CurrentDomain.UnhandledException .
public MainWindow() { InitializeComponent(); AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; } // void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { Exception lException = (Exception)e.ExceptionObject; MessageBox.Show(lException.Message + "\nIsTerminating: " + e.IsTerminating); } // private void button1_Click(object sender, RoutedEventArgs e) { throw new Exception("user exception thrown"); } //
Exceptions (part 2, advanced)
Season’s Greetings!
Today we have a very short post about custom exceptions, which provide more specific information. Of course the custom exception has to inherit from System.Exception. Adding several constructors including a parameterless one is good practice.
The suffix “Exception” in your exception name is convention (eg. “OutOfMemory
Exception“, “FileNotFoundException“). Adding the Serializable attribute can be useful when you work across application domains. Properties help providing extra information.
Don’t use the System.ApplicationException class.
http://msdn.microsoft.com/en-us/library/system.applicationexception.aspx states:
If you are designing an application that needs to create its own exceptions, you should derive custom exceptions from the Exception class. It was originally thought that custom exceptions should derive from the ApplicationException class; however in practice this has not been found to add significant value.
In other words: ApplicationException is a relic of the past, where Microsoft intended developers to inherit all their custom exceptions from. But this has never become practice and custom exceptions now derive from the Exception class.
[Serializable] public class UserNotFoundException : Exception { public string UserId { get; private set; } public UserNotFoundException(string xUserId) : base() { UserId = xUserId; base.HelpLink = "http://www.ohta.de"; } // constructor public UserNotFoundException(string xUserId, string xMessage) : base(xMessage) { UserId = xUserId; base.HelpLink = "http://www.ohta.de"; } // constructor public UserNotFoundException(string xUserId, string xMessage, Exception xInnerException) : base(xMessage, xInnerException) { UserId = xUserId; base.HelpLink = "http://www.ohta.de"; } // constructor protected UserNotFoundException(SerializationInfo xSerializationInfo, StreamingContext xStreamingContext) { UserId = xSerializationInfo.GetValue("UserId", typeof(string)) as string; } // constructor public void GetObjectData(SerializationInfo xSerializationInfo, StreamingContext xStreamingContext) { xSerializationInfo.AddValue("UserId", UserId, typeof(string)); } } // class
Exceptions (part 1, advanced)
Exceptions are not errors, they are exceptions. They should not be used for code that can deal with errors. Exceptions are for situations that cannot be solved like running out of RAM or hard disk space. They are pretty slow, because they deal with the entire stack trace. Here is a short benchmark program:
static double DoSomeCalc(double d) { return d * 1.1; } // static double DoSomeCalc2(Exception e) { throw e; } // static void Exceptions1() { const int n = 10000000; double d = 1.0; Stopwatch lStopwatch = new Stopwatch(); lStopwatch.Start(); for (int i = 0; i < n; i++) d *= 1.1; Console.WriteLine("benchmark ms " + lStopwatch.ElapsedMilliseconds); lStopwatch.Restart(); try { for (int i = 0; i < n; i++) d *= 1.1; } catch (Exception) { throw; } Console.WriteLine("efficient try/catch block ms " + lStopwatch.ElapsedMilliseconds); lStopwatch.Restart(); for (int i = 0; i < n; i++) { try { d *= 1.1; } catch (Exception) { throw; } } Console.WriteLine("inefficient try/catch block ms " + lStopwatch.ElapsedMilliseconds); Console.WriteLine(); lStopwatch.Restart(); for (int i = 0; i < n; i++) d = DoSomeCalc(d); Console.WriteLine("method call, benchmark ms " + lStopwatch.ElapsedMilliseconds); lStopwatch.Restart(); try { for (int i = 0; i < n; i++) d = DoSomeCalc(d); } catch (Exception) { throw; } Console.WriteLine("method call, efficient try/catch block ms " + lStopwatch.ElapsedMilliseconds); lStopwatch.Restart(); for (int i = 0; i < n; i++) { try { d = DoSomeCalc(d); } catch (Exception) { throw; } } Console.WriteLine("method call, inefficient try/catch block ms " + lStopwatch.ElapsedMilliseconds); Console.WriteLine(); Exception e = new Exception(); // only one instance, we exclude the creation time for the object in this test lStopwatch.Restart(); for (int i = 0; i < 100; i++) { try { throw e; } catch (Exception) { } } Console.WriteLine("100 exceptions thrown in ms " + lStopwatch.ElapsedMilliseconds); lStopwatch.Restart(); for (int i = 0; i < 100; i++) { try { DoSomeCalc2(e); } catch (Exception) { } } Console.WriteLine("method call, 100 exceptions thrown in ms " + lStopwatch.ElapsedMilliseconds); lStopwatch.Stop(); Console.ReadLine(); } //
example output:
benchmark ms 2227
efficient try/catch block ms 2179
inefficient try/catch block ms 2201method call, benchmark ms 2448
method call, efficient try/catch block ms 2436
method call, inefficient try/catch block ms 2431100 exceptions thrown in ms 603
method call, 100 exceptions thrown in ms 652
The first three results are in line with each other. The code optimizer does the job and there is no visible impact on the outcome. The difference is more likely to be a result of context switching (threading).
And when we call a method the slowdown is also regular, no big impact of the try/catch blog.
But when exceptions are thrown, the system considerably slows down. 600 ms for just 100 exceptions is a disaster. And it gets even worse when you call a method, because the stack trace becomes longer. Imagine what happens when you have nested methods involved.
The conclusion is that throwing exceptions for the programmer’s convenience is bad practice. Exceptions must be avoided by all means. You’d rather perform a quick zero check than raise a DivideByZeroException.
Do not reuse exception objects, this is not thread safe. In general try to avoid re-throwing exceptions. Anyway, let’s see how to re-throw exceptions in case you need it:
a) Re-“throw” without any identifier. This preserves the original exception details.
static void Exceptions2() { int lZero = 0; try { int i = 4 / lZero; } catch (Exception) { throw; } } // static void Exceptions3() { try { Exceptions2(); } catch (Exception e) { Console.WriteLine("Message: {0}", e.Message); Console.WriteLine("StackTrace: {0}", e.StackTrace); Console.WriteLine("HelpLink: {0}", e.HelpLink); Console.WriteLine("InnerException: {0}", e.InnerException); Console.WriteLine("TargetSite: {0}", e.TargetSite); Console.WriteLine("Source: {0}", e.Source); } } //
Message: Attempted to divide by zero.
StackTrace: at ConsoleApplication1.Program.Exceptions2() in ….\Program.cs:line 1155
at ConsoleApplication1.Program.Exceptions3() in ….\Program.cs:line 1160
HelpLink:
InnerException:
TargetSite: Void Exceptions2()
Source: ConsoleApplication1
b) Re-throw the original exception. Add some more information.
static void Exceptions4() { int lZero = 0; try { int i = 4 / lZero; } catch (DivideByZeroException e) { throw new DivideByZeroException("Division by Zero", e); } catch (Exception e) { throw new Exception("Any Exception", e); } // will not be thrown in this example } // static void Exceptions5() { try { Exceptions4(); } catch (Exception e) { Console.WriteLine("Message: {0}", e.Message); Console.WriteLine("StackTrace: {0}", e.StackTrace); Console.WriteLine("HelpLink: {0}", e.HelpLink); Console.WriteLine("InnerException: {0}", e.InnerException); Console.WriteLine("TargetSite: {0}", e.TargetSite); Console.WriteLine("Source: {0}", e.Source); } } //
example output:
Message: Division by Zero
StackTrace: at ConsoleApplication1.Program.Exceptions4() in ….\Program.cs:line 1134
at ConsoleApplication1.Program.Exceptions5() in ….\Program.cs:line 1140
HelpLink:
InnerException: System.DivideByZeroException: Attempted to divide by zero.
at ConsoleApplication1.Program.Exceptions4() in ….\Program.cs:line 1133
TargetSite: Void Exceptions4()
Source: ConsoleApplication1